#! /usr/bin/env python
###############################################################################
#
# file: md.py
# author: Victor Bos
# 
# Copyright 2007--2011 Victor Bos
#
# This file is part of GREATOR.
#
#    GREATOR is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.
#
#    GREATOR is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.
#
#    You should have received a copy of the GNU General Public License
#    along with GREATOR.  If not, see <http://www.gnu.org/licenses/>.
#
###############################################################################
#
# md stands for map display.
#
######################################################################
#
# TODO:
#
#
# - Import game files (show differences between game files), also
#   merging game files.
#
# - AA-gun should be smaller.
# 
# - Fighter shorter by 1 pixel
#
# - Bomber: needs improvement.
# 
# - Move a of n units at once. Possibly with the option of selecting between
#   1 and n units)
#
# - area browsing: add list of areas under the unit buttons. Sort
#   areas on distance to current area. List is visible only
#   while browsing through areas (Shift-Prior and Shift-Next).
#   See Tkinter's FileDialog.py for an example of list with scrollbars.
#
# - BUG: zoom in/out while defining a new area results
#        in the current points of the area to be lost.
#
# - add more status info during move-edit (destination area, number of
#   steps, number of steps left)
#
# - if program exits, check for unsaved changes and ask for save if needed
#
# - escape the #-character if it is not part of a comment
#
# - add online help
#
# - BUG: after changing the area colour in the Area Editor, the Setup
#   Editor still shows the old colour (until you re-select the changed
#   area).
#
######################################################################

# Standard library imports
import Canvas
import csv
import Dialog
import ScrolledText
import StringIO
import tempfile
import Tkinter
import bisect
import copy
import io
import math
import os.path
import sys
import tkFont
import traceback
import zipfile

import Action
import Area
import AreaEditor
import CanvasUnit
import EditAttrDialog
import EditColourDialog
import EditPlayerDialog
import EditUnitDialog
import gameconf
import graphics
import guiutils
import PasswordDialog
import Player
import Unit
import utils
import UType
import tkSimpleDialog
import ToolTip

        
DEBUG = True
FG_BORDER_WIDTH=5
BOARD_AREA_NAME = "The Game Board"
AREA_OUTLINE_WIDTH=1
# tags for Tkinter-canvas items
COMPARE_UNIT_TAG = "compare_unit"
UNIT_NUM_TAG = "unit_num"
AREA_TAG = "area"
AREA_TEXT_TAG = "area_text"
AREA_POLY_HANDLE = "area_poly_handle"
MAP_TAG = "map"


def load_icon(filename):
    return Tkinter.PhotoImage(file=os.path.join(utils.mdpath(), "..", "icons", filename))

def sign(num):
    if num<0:
        return -1
    else:
        return 1

def quit_command():
    ''' Single-exit-point of the program'''
    sys.exit(0)
    

def rotate_left(l):
    '''
    Returns a copy of l with the elements rotated to the left; the 
    first element will become the last.
    '''
    if len(l)>1:
        return l[1:] + [l[0]]
    return l[:]

def rotate_right(l):
    '''
    Returns a copy of l with the elements rotated to the right; the 
    last element will become the first.
    '''
    if len(l)>1:
        return [l[-1]] + l[:-1]
    return l[:]


def add_bboxes(bba, bbb):
    bb = list(bba)
    if bb[0]>bbb[0]:
        bb[0] = bbb[0]
    if bb[1]>bbb[1]:
        bb[1] = bbb[1]
    if bb[2]<bbb[2]:
        bb[2] = bbb[2]
    if bb[3]<bbb[3]:
        bb[3] = bbb[3]    
    return tuple(bb)

def smaller_font(origfont, deltapt):
    f = tkFont.Font(font=origfont).copy()
    f.configure(size=f["size"]-deltapt)
    return f

######################################################################

def align_to_grid(coord, grid=3):
    #r = coord % grid
    #half_grid = grid / 2.0
    #coord = coord - r
    #if r>=half_grid:
    #    coord += grid
    return coord

    # grid == 3:            h==1.5
    # r==0 -> coord + 0     coord - r
    # r==1 -> coord + -1    coord - r
    # r==2 -> coord + 1     coord + (grid-r)
    # grid == 4:            h==2.0
    # r==0 -> coord + 0
    # r==1 -> coord + -1
    # r==2 -> coord + 2
    # r==3 -> coord + 1
    # grid == 5:            h==2.5
    # r==0 -> coord + 0
    # r==1 -> coord + -1
    # r==2 -> coord + -2
    # r==3 -> coord + 2
    # r==4 -> coord + 4




######################################################################
# Convenience constants and functions for Tkinter

# The colour-depth of Tkinter.PhotoImage objects seems to be 16 (could
# not find this documented anywhere).
TKPHOTOIMAGE_DEPTH = 16

def scalecopy_tk_photoimage(phimg, sf):
    result = None
    if phimg is not None:
        if sf != 1.0:
            w, h = phimg.width(), phimg.height()
            sw, sh = int(round(sf*w)), int(round(sf*h))
            result = phimg.zoom(sw, sh)  
        else:
            result = phimg.copy()
    return result

def same_colour(ca, cb, threshold=0):
    diff = abs(ca[0]-cb[0])
    diff += abs(ca[1]-cb[1])
    diff += abs(ca[2]-cb[2])
    return diff <= threshold

def rgb256_str(rgb_tuple):
    '''
    Returns a string representation of the colour rgb_tuple.
    rgb_tuple is supposed to be an integer 3-tuple with the 
    elements representing the Red, Green, and Blue values.
    Each elements is a value in the range [0,255].
    '''
    return "#%02x%02x%02x" % rgb_tuple

def tt(parent, **kw):
    delay = kw.get('delay', 500)
    ToolTip.ToolTip(parent, delay=delay, **kw)

######################################################################
# following class is from
# http://tkinter.unpythonic.net/wiki/ProgressMeter

class Meter(Tkinter.Frame):
# Michael Lange <klappnase at 8ung.at>
# The Meter class provides a simple progress bar widget for Tkinter.

# INITIALIZATION OPTIONS:
# The widget accepts all options of a Tkinter.Frame plus the following:

#     fillcolor -- the color that is used to indicate the progress of the
#                  corresponding process; default is "orchid1".
#     value -- a float value between 0.0 and 1.0 (corresponding to 0% - 100%)
#              that represents the current status of the process; values higher
#              than 1.0 (lower than 0.0) are automagically set to 1.0 (0.0); default is 0.0 .
#     text -- the text that is displayed inside the widget; if set to None the widget
#             displays its value as percentage; if you do not want any text, use text="";
#             default is None.
#     font -- the font to use for the widget's text; the default is system specific.
#     textcolor -- the color to use for the widget's text; default is "black".

# WIDGET METHODS:
# All methods of a Tkinter.Frame can be used; additionally there are two widget specific methods:

#     get() -- returns a tuple of the form (value, text)
#     set(value, text) -- updates the widget's value and the displayed text;
#                         if value is omitted it defaults to 0.0 , text defaults to None .
#
    def __init__(self, master, width=300, height=20, bg='white', fillcolor='orchid1',\
                 value=0.0, text=None, font=None, textcolor='black', *args, **kw):
        Tkinter.Frame.__init__(self, master, bg=bg, width=width, height=height, *args, **kw)
        self._value = value

        self._canv = Tkinter.Canvas(self, bg=self['bg'], width=self['width'], height=self['height'],\
                                    highlightthickness=0, relief='flat', bd=0)
        self._canv.pack(fill='both', expand=1)
        self._rect = self._canv.create_rectangle(0, 0, 0, self._canv.winfo_reqheight(), fill=fillcolor,\
                                                 width=0)
        self._text = self._canv.create_text(self._canv.winfo_reqwidth()/2, self._canv.winfo_reqheight()/2,\
                                            text='', fill=textcolor)
        if font:
            self._canv.itemconfigure(self._text, font=font)

        self.set(value, text)
        self.bind('<Configure>', self._update_coords)

    def _update_coords(self, event):
        '''Updates the position of the text and rectangle inside the canvas when the size of
        the widget gets changed.'''
        # looks like we have to call update_idletasks() twice to make sure
        # to get the results we expect
        self._canv.update_idletasks()
        self._canv.coords(self._text, self._canv.winfo_width()/2, self._canv.winfo_height()/2)
        self._canv.coords(self._rect, 0, 0, self._canv.winfo_width()*self._value, self._canv.winfo_height())
        self._canv.update_idletasks()

    def get(self):
        return self._value, self._canv.itemcget(self._text, 'text')

    def set(self, value=0.0, text=None):
        #make the value failsafe:
        if value < 0.0:
            value = 0.0
        elif value > 1.0:
            value = 1.0
        self._value = value
        if text == None:
            #if no text is specified use the default percentage string:
            text = str(int(round(100 * value))) + ' %'
        self._canv.coords(self._rect, 0, 0, self._canv.winfo_width()*value, self._canv.winfo_height())
        self._canv.itemconfigure(self._text, text=text)
        self._canv.update_idletasks()
    
    def set_fillcolor(self, fillcolor):
        self._canv.itemconfigure(self._rect, fillcolor=fillcolor)

######################################################################

SCALEABLE = "SCALEABLE" # tag for scaleable canvas items
SCALESTEP = 0.5 * math.sqrt(2.0) # step by which zoom in/out changes the scalefactor

EDIT_AREAS_MODE = "edit areas"
SETUP_MODE = "setup"
COLOURING_MODE = "colouring"
COLOUR_PICK_MODE = "colourpick"

######################################################################


def exec_if_not_None(func):
    if func is not None:
        func()


class Root:


    def __init__(self, tkroot, title='MD'):
        self.tkroot = tkroot
        self.tkroot.protocol("WM_DELETE_WINDOW", self.quit_command)
        
        # Undo/redo user actions via a stack
        self.title = ""
        self.game_title = ""
        self.gameconf = None
        self.cursors = []
        
        self.update_window_title(title=title)
        self.current_colour_var =  Tkinter.StringVar()
        self.colouring = Tkinter.BooleanVar(value = False)
        self.cb_panel = None
        self.init_editors()# creates the editor attributes of Root
        self.create_widgets()
        self.redraw_gui_for_gameconf(gameconf.GameConfiguration())

        self.show_area_name_boxes = False
        
    def set_title(self, title):
        self.title = title

    def set_game_title(self, game_title):
        self.game_title = game_title


    def update_window_title(self, title=None, game_title=None):
        if title is not None:
            self.set_title(title)
        if game_title is None:
            game_title = ""
        self.set_game_title(game_title)
        self.tkroot.title(self.title + " " + self.game_title)

    def push_cursor(self, c):
        self.cursors.append(self.tkroot.cget("cursor"))
        self.tkroot.configure(cursor=c)

    def pop_cursor(self):
        if len(self.cursors)>0:
            self.tkroot.configure(cursor=self.cursors[-1])
            del self.cursors[-1]
        else:
            guiutils.display_error_message("Cannot reset the cursor anymore")

    def add_player(self, p):
        self.gameconf.players.append(p)
        self.is_logged_in[p.name] = False

    def init_gamedata(self, gameConf):
        self.scale_factor = 1.0
        self.unit_order = None# TAB-order for unit selection
        self.gameconf = gameConf
        self.current_colour_var.set(":".join(self.gameconf.colours[0]))
        self.is_logged_in = {}# mapping from string (player name) to boolean 

        self.canvas_unit = {} # mapping from unit names to CanvasUnits
        self.unit_tk_bitmap = {} # mapping from units to tk bitmaps
        
        self.area_for_unit = {}
        self.units_for_area = {}
        self.player_for_unit = {}
        self.units_for_player = {}

        for p in self.gameconf.players:
            self.is_logged_in[p.name] = False            

        self.prev_active_player = None
        self.area_cid = utils.O2ODict()# bijection from area names to cids
        self.unit_cids = set()
        self.current_item_cid = None
        self.current_area_cid = None
        
        self.game_dir = None# directory name of the game
        self.game_map_tk = None # The PhotoImage that is displayed
        self.map_image_cid = None # the map TkPhotoImage on the canvas

    def get_units_in_area(self, area_name):
        units = self.units_for_area.get(area_name, None)
        if units is None:
            units = self.units_for_area[area_name] = set()
        return units

    def get_area_with_unit(self, unit_name):
        return self.area_for_unit.get(unit_name, None)

    def get_units_of_player(self, player_fgbg):
        units = self.units_for_player.get(player_fgbg, None)
        if units is None:
            units = self.units_for_player[player_fgbg] = set()
        return units

    def get_player_with_unit(self, unit_name):
        return self.player_for_unit.get(unit_name, None)

    def compute_units_in_area(self):
        self.units_for_area = {}
        for u in self.gameconf.get_units():
            area = self.gameconf.area_for_unit(u)
            if area.name not in self.units_for_area:
                self.units_for_area[area.name] = set()
            self.units_for_area[area.name].add(u)
        
    def mainloop(self):
        self.tkroot.mainloop()
        
    def create_widgets(self):
        self.create_menubar()
        self.create_maingrid()

    def create_menubar(self):
        self.menubar = Tkinter.Menu(
            self.tkroot,
            type="menubar",
            #font=default_font()
            )
        self.tkroot.config(menu=self.menubar)
        self.create_file_menu()
        self.create_game_menu()
        self.create_edit_menu()

        
    def create_file_menu(self):
        self.file_menu = Tkinter.Menu(
            self.menubar, tearoff=False,
            #font=header_font()
            )
        self.menubar.add_cascade(label="File", menu=self.file_menu)
        self.file_menu.add_command(
            label="Load game",
            command=self.open_load_game_dialog)
        self.file_menu.add_command(
            label="Save game",
            command=self.open_save_game_dialog)
        self.file_menu.add_command(label="Clear all", command=self.clear_game)

        self.file_menu.add_separator()
        self.file_menu.add_command(
            label="Save as text",
            command=self.open_save_setup_as_text_dialog)
        
        self.file_menu.add_separator()
        self.file_menu.add_command(
            label="Save as CSV file",
            command=self.open_save_setup_as_csv_dialog)
        
        self.file_menu.add_command(
            label="Update from CSV file",
            command=self.open_load_setup_as_csv_dialog)

        self.file_menu.add_separator()
        self.file_menu.add_command(label="Quit", command=self.quit_command)

    def create_game_menu(self):
        self.game_menu = Tkinter.Menu(self.menubar, tearoff=False)
        self.menubar.add_cascade(label="Game", menu=self.game_menu)
        self.show_unit_list = Tkinter.BooleanVar(value = False)
        self.game_menu.add_checkbutton(
            label="Show unit list",
            command=self.update_unit_list,
            variable=self.show_unit_list)
        self.game_menu.add_separator()

        self.game_menu.add_command(
            label="Compare game",
            command=self.open_compare_game_dialog)
        
        self.game_menu.add_command(
            label="Edit setup",
            command=self.edit_setup_mode)
        self.game_menu.add_command(
            label="Load setup",
            command=self.open_load_setup_dialog)
        self.game_menu.add_command(
            label="Save setup",
            command=self.open_save_setup_dialog)

    def create_edit_menu(self):
        self.area_menu = Tkinter.Menu(self.menubar, tearoff=False)
        self.menubar.add_cascade(label="Edit", menu=self.area_menu)
        
        self.area_menu.add_command(
            label="Load map",
            command=self.open_load_map_dialog)        

        self.area_menu.add_separator()
        
        self.area_menu.add_command(
            label="Load areas",
            command=self.open_load_areas_dialog)
        self.area_menu.add_command(
            label="Save areas",
            command=self.open_save_areas_dialog)
        self.area_menu.add_command(
            label="Edit areas",
            command=self.edit_areas_mode)
        self.area_menu.add_command(
            label="Delete areas",
            command=self.delete_areas)

        self.area_menu.add_separator()
        self.area_menu.add_command(
            label="Colouring",
            command=self.edit_colouring_mode)
        
        self.area_menu.add_separator()
        self.area_menu.add_command(
            label="Pick colour",
            command=self.colour_pick_mode)

    def empty_gameconf(self):
        return  gameconf.GameConfiguration()
    
    def clear_game(self):
        if (not self.gameconf.ur_stack.can_undo()) or guiutils.askyesno(
                    "Confirm clear game", 
                    "Clear game cannot be undone. Do you want to continue?"):
            self.undraw_game()
            self.redraw_gui_for_gameconf(self.empty_gameconf())

    def delete_maingrid(self):
        self.maingrid.pack_forget()

    def create_maingrid(self):
        self.maingrid = Tkinter.Frame(self.tkroot)

        self.create_info_bar()
        self.create_editor_panel()
        self.maingrid.pack(fill="both", expand="yes")

        self.map_hscrollbar = Tkinter.Scrollbar(self.maingrid)
        self.map_hscrollbar.grid(row=2, column=1, sticky="ew")
        self.map_hscrollbar.config(orient="horizontal")
        
        self.map_vscrollbar = Tkinter.Scrollbar(self.maingrid)
        self.map_vscrollbar.grid(row=1, column=2, sticky="ns")

        self.create_map_canvas()        
        self.maingrid.rowconfigure(1, weight=1)
        self.maingrid.columnconfigure(1, weight=1)
        
        self.create_turn_buttons()
        self.update_unit_list()
        
    def create_info_bar(self):
        self.info_textvar = Tkinter.StringVar()
        self.info_bar = Tkinter.Entry(
            self.maingrid,
            foreground="brown",
            background="khaki",
            textvariable=self.info_textvar)
        self.info_bar.grid(
            row=0,
            column=1,
            columnspan=2,
            sticky="ew")
        self.maingrid.rowconfigure(0, weight=0)

    def get_info_message(self):
        return self.info_textvar.get()

    def set_info_message(self, msg):
        self.info_textvar.set(msg)

    def clear_info_message(self):
        self.info_textvar.set("")

    ## @brief creates a canvas with scrollbars.
    def create_map_canvas(self):
        self.map_canvas = Tkinter.Canvas(self.maingrid)

        self.map_canvas["takefocus"] = Tkinter.TRUE

        self.map_canvas.grid(row=1, column=1, sticky="nesw")

        self.map_hscrollbar.config(command=self.map_canvas.xview)
        self.map_canvas.config(xscrollcommand=self.map_hscrollbar.set)

        self.map_vscrollbar.config(command=self.map_canvas.yview)
        self.map_canvas.config(yscrollcommand=self.map_vscrollbar.set)

        self.map_canvas.bind("<Enter>", lambda e: self.map_canvas.focus_set())

        # mouse-wheel scrolling

        # On win32 (MS Windows) platforms, mouse wheel events are
        # called MouseWheel.
        if sys.platform == 'win32':
            self.map_canvas.bind(
                '<MouseWheel>',
                lambda e: self.wheel_scroller(e.widget.yview_scroll,
                                              -1*sign(e.delta)))
            self.map_canvas.bind(
                '<Shift-MouseWheel>',
                lambda e: self.wheel_scroller(e.widget.xview_scroll,
                                              -1*sign(e.delta)))

        elif sys.platform == "cygwin":
            # no mouse wheel bindings supported. TODO: correct this!
            pass
        else:
            # assume we're on an X-server based platform
            # on X the mouse wheel events are Button-4 and Button-5
            self.map_canvas.bind(
                '<Button-4>',
                lambda e: self.wheel_scroller(e.widget.yview_scroll,-1))
            self.map_canvas.bind(
                '<Button-5>',
                lambda e: self.wheel_scroller(e.widget.yview_scroll,1))


            self.map_canvas.bind(
                '<Shift Button-4>',
                lambda e: self.wheel_scroller(e.widget.xview_scroll,-1))
            self.map_canvas.bind(
                '<Shift Button-5>',
                lambda e: self.wheel_scroller(e.widget.xview_scroll, 1))

        # Bind the arrow keys to scroll commands
        self.map_canvas.bind(
            '<Up>',
            lambda e: self.wheel_scroller(e.widget.yview_scroll, -2))
            
        self.map_canvas.bind(
            '<Down>',
            lambda e: self.wheel_scroller(e.widget.yview_scroll, 2))
            
        self.map_canvas.bind(
            '<Left>',
            lambda e: self.wheel_scroller(e.widget.xview_scroll, -2))
            
        self.map_canvas.bind(
            '<Right>',
            lambda e: self.wheel_scroller(e.widget.xview_scroll, 2))

        # zoom functions are not bound, because they require too
        # much memory (for the wa3r map)
        self.tkroot.bind("<Control 0>", self.zoom_null)
        self.tkroot.bind("<Control a>", self.edit_areas_mode)
        self.tkroot.bind("<Control l>", self.toggle_unit_list)
        self.tkroot.bind("<Control m>", self.toggle_map)
        self.tkroot.bind("<Control minus>", self.zoom_out)
        self.tkroot.bind("<Control o>", self.open_load_game_dialog)
        self.tkroot.bind("<Control plus>", self.zoom_in,)
        self.tkroot.bind("<Control q>", self.quit_command)
        self.tkroot.bind("<Control s>", self.open_save_game_dialog)
        self.tkroot.bind("<Control u>", self.edit_setup_mode)
        self.tkroot.bind("<Control y>", self.game_redo_key_event, add=False)
        self.tkroot.bind("<Control z>", self.game_undo_key_event, add=False)
        self.tkroot.bind("<Key space>", self.select_unit)
        self.tkroot.bind("<Next>", self.select_next_unit)
        self.tkroot.bind("<Prior>", self.select_prev_unit)
        self.tkroot.bind("<Shift Next>", self.select_next_area)
        self.tkroot.bind("<Shift Prior>", self.select_prev_area)
        self.tkroot.bind("<Control Next>", self.select_next_area_handle)
        self.tkroot.bind("<Control Prior>", self.select_prev_area_handle)
        
        self.bind_unit_keys()

    def create_turn_buttons(self):
        self.turn_button_frame = Tkinter.Frame(self.maingrid)

        undo_img = load_icon("undo.gif")
        undo_button = Tkinter.Button(self.turn_button_frame,
                                     #compound=Tkinter.TOP,
                                     #text="Undo",
                                     bg="ghostwhite",
                                     image=undo_img,
                                     command=self.game_undo)
        tt(undo_button, text="Undo")
        undo_button.pack(side=Tkinter.LEFT, expand=False, fill=Tkinter.Y)
        undo_button.img = undo_img

        redo_img = load_icon("redo.gif")
        redo_button = Tkinter.Button(self.turn_button_frame,
                                     bg="ghostwhite",
                                     #compound=Tkinter.TOP,
                                     image=redo_img,
                                     #text="Redo",
                                     command=self.game_redo)
        tt(redo_button, text="Redo")
        redo_button.pack(side=Tkinter.LEFT, expand=False, fill=Tkinter.Y)
        redo_button.img = redo_img
        
        prev_turn_img = load_icon("prev_turn.gif")
        prevturn = Tkinter.Button(self.turn_button_frame, 
                                  bg="ghostwhite",
                                  image=prev_turn_img,
                                  #text="Previous turn",
                                  command=self.previous_turn)
        tt(prevturn, text="Previous turn")
        prevturn.pack(side=Tkinter.LEFT, expand=False, fill=Tkinter.Y)
        prevturn.img = prev_turn_img
        
        next_turn_img = load_icon("next_turn.gif")
        nextturn = Tkinter.Button(self.turn_button_frame, 
                                  bg="ghostwhite",
                                  image=next_turn_img,
                                  #text="Next turn",
                                  command=self.next_turn)
        tt(nextturn, text="Next turn")
        nextturn.pack(side=Tkinter.LEFT, expand=False, fill=Tkinter.Y)
        nextturn.img = next_turn_img
        
        turn_name_label = Tkinter.Label(self.turn_button_frame,
                                  bg="ghostwhite",
                                  text="Turn:")
        tt(turn_name_label, text="Name of the current turn")

        turn_name_label.pack(side=Tkinter.LEFT, expand=False, fill=Tkinter.Y)
        self.turn_name_var = Tkinter.StringVar()
        self.turn_name_var.set("turn?")
        self.turn_name = Tkinter.Entry(self.turn_button_frame, 
                                       textvariable=self.turn_name_var)
        self.turn_name.pack(side=Tkinter.LEFT, expand=True, fill=Tkinter.BOTH)
        self.turn_name.bind("<Return>", self.turn_name_entry_cmd)

        new_turn_img = load_icon("new_turn.gif")
        newturn = Tkinter.Button(self.turn_button_frame,
                                 bg="ghostwhite",
                                 image=new_turn_img,
                                 #text="New turn",
                                 command = self.new_turn)
        tt(newturn, text="Create a new turn")        
        newturn.pack(side=Tkinter.LEFT, expand=False, fill=Tkinter.Y)
        newturn.img = new_turn_img
        
        clear_undo_redo_img = load_icon("clear_undo_redo.gif")
        clear_undo_redo = Tkinter.Button(self.turn_button_frame,
                                         bg="ghostwhite",
                                         image=clear_undo_redo_img,
                                         #text="Clear undo/redo",
                                         command = self.clear_undo_redo)
        tt(clear_undo_redo, text="Clear all undo/redo information")

        clear_undo_redo.pack(side=Tkinter.LEFT, expand=False, fill=Tkinter.Y)
        clear_undo_redo.img = clear_undo_redo_img
        
        self.turn_button_frame.grid(row=3, column=1, sticky="ew")
        

    def has_previous_turn(self):
        return len([ a for a in self.gameconf.ur_stack.undo_stack 
                    if isinstance(a, Action.NewTurnAction)]) > 0

    def has_next_turn(self):
        return len([ a for a in self.gameconf.ur_stack.redo_stack 
                    if isinstance(a, Action.NewTurnAction)]) > 0
    
    def previous_turn(self):
        if self.has_previous_turn():
            self.game_undo()
            while not isinstance(self.gameconf.ur_stack.redo_top(), Action.NewTurnAction):
                self.game_undo()
        else:
            guiutils.display_warning_message("There is no previous turn.")
    
    def next_turn(self):
        if self.has_next_turn():
            self.game_redo()
            while not isinstance(self.gameconf.ur_stack.undo_top(), Action.NewTurnAction):
                self.game_redo()
        else:
            guiutils.display_warning_message("There is no next turn.")
    
    def new_turn(self):
        
        turnname = tkSimpleDialog.askstring("Provide turn name", "Turn name:")
        if turnname is None:
            return
        
        if( not self.gameconf.ur_stack.can_redo()) or guiutils.askyesno(
            "Confirm new turn", 
            "Undone actions cannot be re-done after this. Continue?"):
            self.gameconf.ur_stack.push_action(Action.NewTurnAction(self.gameconf.name, turnname), self)
    
    def clear_undo_redo(self):
        if ((not self.gameconf.ur_stack.is_empty())
            or guiutils.askyesno("Confirm clear undo/redo",
                        "This will delete all undo/redo information; only "
                        "the current configuration will be left.\n\n"
                        "Typically, this is done only to create an initial "
                        "configuration. Continue?")):
            self.gameconf.ur_stack.clear()

    def set_conf_name(self, name):
        self.gameconf.name = name
        self.turn_name_var.set(name)
        
    def turn_name_entry_cmd(self, event):
        tn = self.turn_name_var.get()
        if (tn != "") and (tn != self.gameconf.name):
            self.gameconf.ur_stack.push_action(Action.RenameTurnAction(self.gameconf.name, tn), self)
        else:
            self.turn_name_var.set(self.gameconf.name)
        
    def bind_unit_keys(self):
        self.map_canvas.bind("<p>", self.unit_to_prev_location)
        self.map_canvas.bind("<c>", self.commit_all_units_changes)
        self.map_canvas.bind("<C>", self.commit_all_units_changes)
        
        self.map_canvas.bind("<i>", self.show_current_item_info)

    def show_current_item_info(self, e):
        if self.current_item_cid is not None:
            if self.is_unit_cid(self.current_item_cid):
                cunit = self.canvas_unit[self.get_unit_for_cid(self.current_item_cid)]
                cunit.show_unit_attrs(None)

    def is_unit_drag_ok(self):
        return self.mode == SETUP_MODE

    def unit_to_prev_location(self, e):
        unit = self.get_unit_for_cid(self.current_item_cid)
        if unit is not None:
            if unit.nr_move_dests()>0:

                last_x, last_y = unit.last_move_xy()
                self.gameconf.ur_stack.push_action(Action.MoveUnitToPrevLocAction(unit.name, last_x, last_y), self)
                
            else:
                guiutils.display_warning_message(
                    "There is no previous location for this unit.")


    def update_unit_order(self):
        self.unit_order = None

    def units_in_area(self, acid):
        if acid is None:
            return []
        x0, y0, x1, y1 = self.map_canvas.bbox(acid)
        units = [ ucid 
                  for ucid in self.map_canvas.find_enclosed(x0, y0, x1, y1)
                  if (self.is_unit_cid(ucid) and
                      self.is_unit_in_current_area(ucid)) ]
        return units
        
    def get_unit_order(self):
        if self.unit_order is None:
            self.unit_order = []
            if self.current_area_cid is not None:                
                self.unit_order = rotate_right(
                    self.units_in_area(self.current_area_cid))
        return self.unit_order
    
    def next_unitcid(self, ucidpred=None):
        return self.rotate_unit_order(rotate_left, ucidpred)        
    
    def prev_unitcid(self, ucidpred=None):
        return self.rotate_unit_order(rotate_right, ucidpred)
        
    def rotate_unit_order(self, rotate_func, ucidpred=None):
        '''
        Rotates the unit_order list using the given rotate function.
        At least one rotation is performed. After that, rotation stops
        as soon as ucidpred holds for the current (first) unit-cid
        in unit_order.
        
        '''
        if ucidpred is None:
            ucidpred = lambda ucid: True
        uorder = self.get_unit_order()
        n = len(uorder)
        while n>0:
            self.unit_order = uorder = rotate_func(uorder)
            n -= 1
            ucid = uorder[0]
            if ucidpred(ucid):
                return ucid
        return None

        
    def is_unit_in_current_area(self, ucid):
        unit = self.get_unit_for_cid(ucid)
        if unit is None:
            return False
        x, y = unit.last_move_xy()
        return self.area_cid_for_unit_pos(x, y) == self.current_area_cid
    
    def select_next_unit(self, e):
        ucid = self.next_unitcid(ucidpred = self.is_unit_in_current_area)
        if ucid is not None:
            self.set_current_item_cid(ucid)
            self.select_unit()
        
    def select_prev_unit(self, e):
        ucid = self.prev_unitcid(ucidpred = self.is_unit_in_current_area)
        if ucid is not None:
            self.set_current_item_cid(ucid)
            self.select_unit()

    def select_unit(self, event=None):
        if self.is_unit_cid(self.current_item_cid):
            unitcid = self.current_item_cid
            u = self.get_unit_for_cid(unitcid)
            x, y = u.last_move_xy()
            area_cid = self.area_cid_for_unit_pos(x, y)
            self.center_view_on_area(self.get_area_for_cid(area_cid))
            self.canvas_unit[u].raise_to_top()

    def are_neighbouring_areas(self, aname0, aname1):
        return self.gameconf.get_area_info().are_neighbours(aname0, aname1)
    
    def check_unit_move(self, unit):
        if unit.nr_move_dests()>0:
            x0, y0 = unit.move_dest[0]
            area_cid0 = self.area_cid_for_unit_pos(x0, y0)                
            aname0 = self.area_cid.inv[area_cid0]
            
            overlap = True
            for x1, y1 in unit.move_dest[1:]:
                area_cid1 = self.area_cid_for_unit_pos(x1, y1)
                aname1 = self.area_cid.inv[area_cid1]
                if not self.are_neighbouring_areas(aname0, aname1):
                    overlap = False
                aname0 = aname1
            if not overlap:
                guiutils.display_warning_message(
                    "Unit moves between areas without a common border.")
            
    def is_gamemaster_active(self):
        return self.is_authorized_player(self.get_game_master())

    def commit_all_units_changes(self, e):
        '''
        Commits visibility and position changes for all units.
        '''
        if not self.is_gamemaster_active():
            guiutils.display_warning_message("Activate game master to commit move(s)")
        else:
            unames_moves = dict(
                [(u.name, u.move_dest[:])
                 for u in self.gameconf.get_units()
                 if u.nr_move_dests()>0
                 ]) 

            if len(unames_moves)>0:
                self.gameconf.ur_stack.push_action(Action.CommitUnitsAction(unames_moves), self)

    
    def commit_unit(self, u):
        '''
        Unit u will moved to its final move destination.
        '''
        u.execute_move()
        self.draw_unit(u)

    def update_conf(self, newconf):
        if newconf is not None:
            if newconf is not self.gameconf:
                self.undraw_game()
                self.redraw_gui_for_gameconf(newconf)
        
    def game_undo_key_event(self, dummy):
        self.game_undo()
        return "break"
    
    def game_undo(self):
        if self.gameconf.ur_stack.can_undo():
            self.gameconf.ur_stack.undo(root=self)
        else:
            guiutils.display_error_message("There is nothing to undo.")
            
    def game_redo_key_event(self, dummy):
        self.game_redo()
        return "break"

    def game_redo(self):
        if self.gameconf.ur_stack.can_redo():
            self.gameconf.ur_stack.redo(root=self)
        else:
            guiutils.display_error_message("There is nothing to redo.")
    
    def quit_command(self, args=None):
        if ((not self.gameconf.ur_stack.can_undo()) or
            (not self.gameconf.ur_stack.has_unsaved_changes()) or 
            guiutils.askyesno(
                    "Confirm quit", 
                    "There are unsaved changes. Do you want to continue?")):
            quit_command()
        
    def wheel_scroller(self, scroll_func, delta):
        scroll_func(delta, Tkinter.UNITS)        
        
    def init_editors(self):
        self.area_editor = None
        self.setup_editor = None
        self.colouring_editor = None
        self.colour_pick_editor = None
        self.setup_editor_keys = [
            ("<Button-1>",
             self.left_click_cmd,
             "If on unit, then the unit is selected, otherwise a unit is placed."),

            ("<Shift Button-3>",
             self.shift_right_click_cmd,
             "Edit area at mouse pointer"),

            ("<Delete>",
             self.delete_key,
             "Delete selected"),
            # handy tool to find out about keysyms (see print_key() below)
            #            ("<Key>",
            #             self.print_key,
            #             "print key sym")
           ]
        self.area_editor_keys = [
            ("<Button-1>", self.find_selected_area, "Selects area at mouse pointer."),
            ("<Button-3>", self.define_next_point, "Sets the first or a next point of a new area."),
            ("<Shift Button-3>", self.define_last_point, "Completes the new area."),
            ("<Delete>", self.remove_current_area, "Deletes the currently selected area."),
            ("<Control Up>", self.move_up_area_handle, "Moves up the area handle"),
            ("<Control Down>", self.move_down_area_handle, "Moves down the area handle"),
            ("<Control Left>", self.move_left_area_handle, "Moves left the area handle"),
            ("<Control Right>", self.move_right_area_handle, "Moves right the area handle"),
            ]

        self.colouring_editor_keys = [
            ("<Shift Button-1>",
             self.left_click_colouring_cmd,
             "Update the colour of selected unit or area with current colour"),
            ]

        self.colour_pick_keys = [
            ("<Button-1>",
             self.left_click_colour_pick,
             "Pick the colour of the pixel at mouse point"),
            ]
        # start up with setup editor active.
        self.mode = SETUP_MODE
        self.stop_editor_func = self.stop_setup_mode
        self.show_editor_func = self.show_setup_editor
        self.editor_keys = self.setup_editor_keys


    def print_key(self, e):
        print "keysym=%s, char=%s" % (e.keysym, e.char)
        
    # The editor panel is a place to put different mode-dependent
    # editors, like area-editor, unit-editor.
    def create_editor_panel(self):
        self.edit_panel = Tkinter.Frame(
            self.maingrid)
        self.show_editor_panel()
        
    def show_editor_panel(self):
        self.maingrid.columnconfigure(0, weight=0)
        self.edit_panel.grid(
            row=0,
            rowspan=4,
            column=0,
            sticky="nesw",
            padx=5,
            pady=5)

    def find_selected_unit_or_area(self, e):
        x, y = self.get_canvas_point(e.x, e.y)
        return self.find_selected_unit_or_area_at(x, y)
        
    def find_selected_unit_or_area_at(self, x, y):
        '''
        Searches first for a unit at canvas coordinates (x, y), then, if no
        unit was found, for an area at canvas coordinates (x, y).

        Returns the cid of the unit or area found, or None if nothing
        was found.
        '''
        unit_cid = self.find_selected_unit_at(x, y)
        if unit_cid is None:
            return self.find_selected_area_at(x, y)
        return unit_cid
        
    def find_selected_unit_at(self, x, y):
        '''
        Returns the cid of the topmost unit at canvas coordinates (x,y).
        This unit will also become the currently selected unit. 
        '''
        item_cid = self.find_unit_cid(x, y)
        if item_cid is not None:
            self.set_current_item_cid(item_cid)
        return item_cid

    def find_selected_area_at(self, x, y):
        '''
        Returns the cid of the area containing canvas coordinates (x,y).
        This area (if any) will also become the currently selected area.
        '''
        item_cid = self.find_area_cid(x, y)
        self.select_area_cid(item_cid)
        return item_cid

    def select_next_area(self, e, delta = 1):
        ainfo = self.gameconf.get_area_info()
        if (self.current_area_cid is None) or (not ainfo.get_areas()):
            return

        area = self.get_area_for_cid(self.current_area_cid)
        if area is not None:
            next_area = ainfo.next_area(area, delta)
            self.select_area_cid(self.area_cid[next_area.name])
            x, y = next_area.gravity_point()
            self.center_view_on_point(x, y, 0.25)

    def select_prev_area(self, e):
        self.select_next_area(e, delta = -1)

    def get_unit_for_cid(self, cid):
        for u in self.gameconf.get_units():
            if self.unit_cid(u) == cid:
                return u
        return None

    def get_area_for_cid(self, cid):
        return self.get_area_for_name(self.area_cid.inv[cid])
    
    def get_area_for_name(self, aname):
        return self.gameconf.get_area_info().areas.get(aname, None)
    
    def get_item_type(self, cid):
        if self.is_area_cid(cid):
            return self.area_cid.inv[cid]
        else:
            unit = self.get_unit_for_cid(cid)
            if unit is not None:
                result = unit.name
                area_name = self.area_name_for_unit(unit)
                if area_name is not None:
                    result += " in " + area_name
                return result
        return ""

    def get_current_item_type(self):
        if self.current_item_cid is not None:
            return self.get_item_type(self.current_item_cid)
        return ""
    
    def unit_cid(self, unit):
        cu = self.canvas_unit.get(unit, None)
        if cu is not None:
            return cu.pic_cid
        return None
    
    #############################################################
    # Area editor functions
    #############################################################

    def remove_area_editor(self):
        if self.area_editor is not None:
            if self.stop_editor_func == self.stop_edit_areas_mode:
                self.stop_editor_func()
            self.area_editor.destroy()
            self.area_editor = None

    def create_area_editor(self):
        self.area_editor = AreaEditor.AreaEditor(self.edit_panel)
        self.area_editor.name_entry.bind("<Return>", self.area_name_entry_cmd)
        self.cur_poly = []

    def find_selected_area(self, e):
        if self.cur_poly == []:
            x, y = self.get_canvas_point(e.x, e.y)
            return self.select_area_cid(self.find_area_cid(x, y))
        
    def area_name_entry_cmd(self, event):
        oldname = self.get_current_area_name()
        if oldname is not None:
            oldcid = self.area_cid[oldname]
            newname = self.area_editor.name_textvariable.get()
            if newname != oldname:
                if newname not in self.area_cid:
                    self.gameconf.get_area_info().rename_area(oldname, newname)
                    del self.area_cid[oldname]
                    self.area_cid[newname] = oldcid
                    self.set_current_item_cid(oldcid)
                    self.undraw_area_text(oldname)
                    self.draw_area_text(newname)
                else:
                    guiutils.display_error_message("Area %s already exists" % newname)
        
                       
        
    def show_area_editor(self):
        if self.area_editor == None:
            self.create_area_editor()
        self.area_editor.visible = True
        self.area_editor.pack(
            side=Tkinter.TOP,
            fill=Tkinter.BOTH)
        self.show_areas()
        self.start_new_poly(self.cur_poly)
        
    def hide_area_editor(self):
        self.area_editor.pack_forget()
        self.area_editor.visible = False        
        self.hide_areas()





    ###########################################################

    def get_current_area_name(self):
        if self.current_area_cid is not None:
            return self.area_cid.inv.get(self.current_area_cid, None)
        return None

    def set_area_outlinecolour(self, area_cid, outlinecolour):
        self.map_canvas.itemconfigure(
            area_cid,
            outline = outlinecolour)

    def has_areas(self):
        return self.gameconf.get_area_info().has_areas()
    
    def hide_areas(self):
        if self.has_areas() and self.map_image_cid is not None:            
            self.map_canvas.tag_lower(AREA_TAG, MAP_TAG)
            self.map_canvas.tag_lower(AREA_POLY_HANDLE, MAP_TAG)
            self.map_canvas.tag_raise(AREA_TEXT_TAG, MAP_TAG)

    def show_areas(self):
        if self.has_areas() and self.map_image_cid is not None:
            self.map_canvas.tag_raise(AREA_TAG, MAP_TAG)
            self.map_canvas.tag_raise(AREA_POLY_HANDLE, AREA_TAG)
            self.map_canvas.tag_raise(AREA_TEXT_TAG, AREA_TAG)
        
    def highlight_area(self, area_cid, colour):
        if self.mode != SETUP_MODE:
            self.set_area_outlinecolour(area_cid, colour)
            area = self.get_area_for_cid(area_cid)
            if area is not None:
                nbs = self.get_area_neighbours(area.name)
                for n in nbs:
                    n_cid = self.area_cid.get(n, None)
                    if n_cid is not None:
                        self.set_area_outlinecolour(n_cid, "white")
                        self.map_canvas.tag_raise(n_cid, AREA_TAG)
                self.map_canvas.tag_raise(area_cid, AREA_TAG)
                self.map_canvas.tag_raise(AREA_TEXT_TAG, AREA_TAG)

    def unhighlight_area(self, cid):
        self.set_area_outlinecolour(cid, "brown")
        area = self.get_area_for_cid(cid)
        if area is not None:
            nbs = self.get_area_neighbours(area.name)
            for n in nbs:
                n_cid = self.area_cid.get(n, None)
                if n_cid is not None:
                    self.set_area_outlinecolour(n_cid, "brown")

    def highlight_item(self, cid, colour):
        if self.is_area_cid(cid):
            self.highlight_area(cid, colour)
        elif self.is_unit_cid(cid):
            unit = self.get_unit_for_cid(cid)
            if unit is not None:
                self.canvas_unit[unit].highlight()
        else:
            pass
            
    def unhighlight_item(self, cid):
        if self.is_area_cid(cid):
            self.unhighlight_area(cid)
        else:
            unit = self.get_unit_for_cid(cid)
            if (unit is not None) and (self.canvas_unit.get(unit, None) is not None):
                self.canvas_unit[unit].unhighlight()
            
    def unhighlight_current_item(self):
        if self.current_item_cid is not None:
            self.unhighlight_item(self.current_item_cid)
        
    def find_item_cids(self, x1, y1, x2=None, y2=None, canvas_item_check_func=None):
        '''
        Finds the cids of the items containing the rectangle with canvas coordinates
        (x1, y1) - (x2, y2) and which satisfies the boolean function
        canvas_item_check_func.

        Returns a list of cids of items found, possibly [].
        '''
        if x2 is None:
            x2 = x1+1
        if y2 is None:
            y2 = y1+1
            
        items = [ i for i in self.map_canvas.find_overlapping(x1, y1, x2, y2)
                  if canvas_item_check_func(i) ]
        return items

    def find_all_item_cids(self, canvas_item_check_func=None):
        return [i for i in self.map_canvas.find_all()
                if canvas_item_check_func(i)]
    
    def is_area_cid(self, cid):
        return cid in self.area_cid.inv
    
    def has_area_name(self, name):
        return name in self.area_cid
    
    def find_area_cid(self, x, y):
        '''
        Returns the cid of the area containing the canvas coordinates (x, y).
        '''
        area_cids = self.find_item_cids(x, y, None, None, self.is_area_cid)
        if len(area_cids) == 0:
            return None
        # find the area with smallest bbox and store it in area_cids[0]
        i = 1
        minbba = graphics.bbox_area(self.map_canvas.bbox(area_cids[0]))
        while i<len(area_cids):
            bba = graphics.bbox_area(self.map_canvas.bbox(area_cids[i]))
            if bba<minbba:
                # swap area_cids 0 and i and update minbba
                area_cids[0], area_cids[i] = area_cids[i], area_cids[0]
                minbba = bba
            i += 1
        return area_cids[0]

    def is_unit_or_area_cid(self, cid):
        return self.is_unit_cid(cid) or self.is_area_cid(cid)
    
    def is_unit_cid(self, cid):
        if cid is None:
            return False
        return cid in self.unit_cids
    
    def find_unit_cids(self, x, y):
        '''
        Returns the cids of the units located at canvas coordinates (x,y).
        '''
        return self.find_item_cids(x, y, None, None, self.is_unit_cid) 

    def find_unit_cid(self, x, y):
        '''
        Returns the topmost unit located at canvas coordinates (x,y).
        '''
        result = [None] + self.find_unit_cids(x, y)
        return result[-1]

    def area_name_for_unit(self, unit):
        '''
        Returns the name of the area where unit is currently located.
        Note: not the area(s) into which unit might have move destinations.
        '''
        area = self.gameconf.area_for_unit(unit)
        if area is None:
            guiutils.display_error_message("GameConfiguration has no area for unit: %s" % unit.name) 
            return None
        return area.name

    def sort_by_colour_then_area(self, units):
        def compare(u1, u2):
            a1 = self.area_name_for_unit(u1)
            a2 = self.area_name_for_unit(u2)
            return cmp((u1.colour, a1), (u2.colour,a2))
        return units.sort(compare)

    def area_cid_for_unit_pos(self, x, y):
        area = self.gameconf.area_for_pos(x, y)
        if area is None:
            return None
        return self.area_cid[area.name]
    
    def area_cid_for_unit(self, unit):
        return self.area_cid_for_unit_pos(unit.x, unit.y)
    
    def set_current_item_cid(self, cid):
        '''
        Sets the current item cid to cid. This item, if not None,
        is also highlighted. This method calls unset_current_item first.
        So, the previous current item will be unhighlighted.
        If the item is an area, this will become the current area.
        If the item is a unit, this will become the current unit and
        if the unit is in an area, this will become the current area.
        '''
        self.unset_current_item_cid()
        self.current_item_cid = cid
        msg = self.get_current_item_type()
        self.set_info_message(msg)
        unit = self.get_unit_for_cid(self.current_item_cid)
        if unit is not None:
            x, y = unit.last_move_xy()
            area_cid = self.area_cid_for_unit_pos(x, y)
        else:
            area_cid = cid
        if area_cid is not None:
            if area_cid != self.current_area_cid:
                self.current_area_cid = area_cid
                self.update_unit_order()
            if unit is None:
                area = self.get_area_for_cid(area_cid)
        else:
            self.current_area_cid = None
            self.update_unit_order()
        if self.current_item_cid is not None:
            self.highlight_item(self.current_item_cid, "yellow")
        return cid
    
    def unset_current_item_cid(self):
        self.unhighlight_item(self.current_item_cid)
        name = self.area_cid.inv.get(self.current_item_cid, None)
        if name is not None:
            self.unhighlight_area_namebox(name)
            self.unhighlight_area_poly_handle(name)
        self.current_item_cid = None
        self.clear_info_message()
        
    def select_area_cid(self, area_cid):
        self.set_current_item_cid(area_cid)
        if area_cid is not None:
            name = self.area_cid.inv.get(area_cid, "")
            self.highlight_area_namebox(name)
            if self.area_editor is not None:
                self.area_editor.name_textvariable.set(name)
            self.highlight_area_poly_handle(name)
        return area_cid

    def start_new_poly(self, poly):
        self.cur_poly = poly[:]
        self.draw_poly()

    def undraw_poly(self):
        self.map_canvas.delete("newpoly")

    def draw_area_text(self, area_name):
        if not self.show_area_name_boxes:
            return
        
        area = self.gameconf.get_area_info().areas[area_name]
        if area_name == BOARD_AREA_NAME:
            #print "*drawing", area_name, "x, y:", area.gravity_point()
            return

        gpx, gpy = area.gravity_point()
        gpx, gpy = self.scale_factor*gpx, self.scale_factor*gpy
        area_name_tag = utils.tag_for_name(area_name)
        tags = (area_name_tag+'_text', AREA_TEXT_TAG)

        cid0 = self.map_canvas.create_text(gpx, 
                                           gpy, 
                                           text=area_name, 
                                           tags=tags)
        bbox = self.map_canvas.bbox(cid0)
        #print "drawing", area_name, "x, y:", gpx, gpy
        tags = (area_name_tag+'_textbox', AREA_TEXT_TAG)
        cid1 = self.map_canvas.create_rectangle(
            bbox[0], bbox[1], 
            bbox[2], bbox[3],
            width=0,
            tags=tags)
        self.update_area_name_label(area)
        self.map_canvas.tag_raise(cid0, cid1)
        self.map_canvas.tag_bind(
            cid0,
            "<ButtonPress-1>",
            lambda e, cid0=cid0, cid1=cid1: self.start_drag_area_name(e, cid0, cid1))
        self.map_canvas.tag_bind(
            cid0,
            "<ButtonRelease-1>",
            lambda e, cid0=cid0, cid1=cid1: self.end_drag_area_name(e, cid0, cid1))
        self.map_canvas.tag_bind(
            cid0,
            "<Motion>",
            lambda e, cid0=cid0, cid1=cid1: self.drag_area_name(e, cid0, cid1))

    def highlight_area_namebox(self, area_name):
        tag = utils.tag_for_name(area_name)
        self.map_canvas.itemconfigure(tag+"_textbox", outline="white", width=3)
        
    def unhighlight_area_namebox(self, area_name):
        tag = utils.tag_for_name(area_name)
        self.map_canvas.itemconfigure(tag+"_textbox", outline="", width=0)
        
    def undraw_area_text(self, area_name):
        area_name_tag = utils.tag_for_name(area_name)
        self.map_canvas.delete(area_name_tag + '_text')
        self.map_canvas.delete(area_name_tag + '_textbox')

    def start_drag_area_name(self, event, cid0, cid1):
        self.drag_cid = cid0
        area_name = self.map_canvas.itemcget(cid0, "text")
        self.select_area_cid(self.area_cid[area_name])        

    def is_dragging(self, cid0):
        try:
            return self.drag_cid == cid0
        except:
            return False

    def drag_area_name(self, event, cid0, cid1):
        if self.is_dragging(cid0):
            x0, y0 = self.map_canvas.coords(cid0)
            x1, y1 = self.get_canvas_point(event.x, event.y)
            dx = x1-x0
            dy = y1-y0            
            self.map_canvas.move(cid0, dx, dy)
            self.map_canvas.move(cid1, dx, dy)

    def end_drag_area_name(self, event, cid0, cid1):
        if self.is_dragging(cid0):
            x, y = self.compute_real_xy(self.get_canvas_point(event.x, event.y))
            area_name = self.map_canvas.itemcget(cid0, "text")
            if area_name in self.gameconf.get_area_info().areas:
                area = self.gameconf.get_area_info().areas[area_name]
                area.gpoint = (x, y)
                
        self.drag_cid = None

    def set_area_colours(self, area_name, fg, bg):
        area = self.gameconf.get_area_info().areas[area_name]
        area.set_fgcolour(fg)
        area.set_bgcolour(bg)

    def update_area_name_label(self, area):
        if not self.show_area_name_boxes:
            return
        
        if area.name == BOARD_AREA_NAME:
            return
        area_name_tag = utils.tag_for_name(area.name)
        fg, bg = area.fgcolour(), area.bgcolour()
        text_colour = fg
        if text_colour is Area.TRANSPARENT:
            text_colour = "black"
        self.map_canvas.itemconfigure(area_name_tag+'_text', fill = text_colour)
        # if needed, the name label background can be a shade of the
        # real background:
        # bg = self.colour_fade(bg, 0.1, fg)
        self.map_canvas.itemconfigure(area_name_tag+'_textbox', fill = bg)
     
    def draw_area(self, area_name):
        area = self.gameconf.get_area_info().areas[area_name]
        poly = area.polygon

        p = []
        for (x,y) in poly:
            cx, cy = self.compute_cvs_xy((x, y))
            p.append(align_to_grid(cx, grid=5))
            p.append(align_to_grid(cy, grid=5))
        cid = self.map_canvas.create_polygon(p)
        self.area_cid[area_name] = cid

        tags = (SCALEABLE, utils.tag_for_name(area_name), AREA_TAG)
        self.map_canvas.itemconfigure(cid,
                                      fill = Area.TRANSPARENT,
                                      width = AREA_OUTLINE_WIDTH,
                                      outline = "brown",
                                      tags = tags)

        self.draw_area_poly_handle(area)
        self.draw_area_text(area_name)

    def area_handle_tag(self, area_name):
        return "handle_%s" % utils.tag_for_name(area_name)

    def highlight_area_poly_handle(self, aname):
        self.map_canvas.itemconfigure(self.area_handle_tag(aname),
                                      outline = "red",
                                      fill = "yellow",
                                      width = 3)

    def unhighlight_area_poly_handle(self, aname):
        self.map_canvas.itemconfigure(self.area_handle_tag(aname),
                                      outline = "black",
                                      fill = "white",
                                      width = 1)

    def draw_area_poly_handle(self, area):
        cx, cy = self.compute_cvs_xy(area.poly_start_point())
        tags = (SCALEABLE, AREA_TAG, AREA_POLY_HANDLE, self.area_handle_tag(area.name))
        cid = self.draw_circle(
            cx, 
            cy, 
            5, 
            "black", 
            "white", 
            tags)
        self.map_canvas.tag_bind(
            cid,
            "<ButtonPress-1>",
            lambda e, cid=cid, area=area: self.start_drag_area_handle(e, cid, area))
        self.map_canvas.tag_bind(
            cid,
            "<ButtonRelease-1>",
            lambda e, cid=cid, area=area: self.end_drag_area_handle(e, cid, area))
        self.map_canvas.tag_bind(
            cid,
            "<Motion>",
            lambda e, cid=cid, area=area: self.drag_area_handle(e, cid, area))

    def start_drag_area_handle(self, e, cid, area):
        self.drag_cid = cid

    def end_drag_area_handle(self, e, cid, area):
        if self.is_dragging(cid):
            x, y = self.compute_real_xy(self.get_canvas_point(e.x, e.y))

            # undraw and redraw area is not optimal;
            # only the polygon+handle needs to be updated.
            self.undraw_area(area.name)            

            ainfo = self.gameconf.get_area_info()
            ainfo.move_area_polygon_start(area.name, x, y)

            self.draw_area(area.name)

            self.select_area_cid(self.area_cid[area.name])
        self.drag_cid = None

    def drag_area_handle(self, e, cid, area):
        if self.is_dragging(cid):
            x0, y0, x1, y1 = self.map_canvas.coords(cid)
            newx, newy = self.get_canvas_point(e.x, e.y)
            dx = newx-(x0 + ((x1-x0)/2))
            dy = newy-(y0 + ((y1-y0)/2))            
            self.map_canvas.move(cid, dx, dy)

    def move_area_handle(self, dx, dy):
        area = self.get_area_for_cid(self.current_area_cid)
        if area is not None:
            
            x, y = area.poly_start_point()

            # undraw and redraw area is not optimal;
            # only the polygon+handle needs to be updated.
            self.undraw_area(area.name)            

            ainfo = self.gameconf.get_area_info()
            ainfo.move_area_polygon_start(area.name, x+dx, y+dy)

            self.draw_area(area.name)

            self.select_area_cid(self.area_cid[area.name])
        
    def move_up_area_handle(self, e):
        self.move_area_handle(0, -1)

    def move_down_area_handle(self, e):
        self.move_area_handle(0, 1)

    def move_left_area_handle(self, e):
        self.move_area_handle(-1, 0)

    def move_right_area_handle(self, e):
        self.move_area_handle(1, 0)
            

    def select_next_area_handle(self, e):
        if self.is_area_cid(self.current_item_cid):
            area = self.get_area_for_cid(self.current_item_cid)
            area.incr_poly_idx()
            self.undraw_area(area.name)
            self.draw_area(area.name)
            self.select_area_cid(self.area_cid[area.name])


    def select_prev_area_handle(self, e):
        if self.is_area_cid(self.current_item_cid):
            area = self.get_area_for_cid(self.current_item_cid)
            area.decr_poly_idx()
            self.undraw_area(area.name)
            self.draw_area(area.name)
            self.select_area_cid(self.area_cid[area.name])
        

    def undraw_area_poly_handle(self, aname):
        self.map_canvas.delete(self.area_handle_tag(aname))

    def make_neighbouring_areas(self, aname1, aname2):
        self.gameconf.get_area_info().make_neighbours(aname1, aname2)

    def get_area_neighbours(self, aname):
        return self.gameconf.get_area_info().get_neighbours(aname)

    def edit_area(self):
        if self.is_area_cid(self.current_item_cid):
            area = self.get_area_for_cid(self.current_item_cid)
            ead = EditAttrDialog.EditAreaDialog(
                self.tkroot,
                height = 300)
            attrs = copy.copy(area.attrs)
            attrs['name'] = area.name
            ead.init_attrs(attrs)
            result = ead.go()
            if result == EditAttrDialog.DELETE:
                nbs = self.get_area_neighbours(area.name)
                self.gameconf.ur_stack.push_action(Action.RemoveAreaAction(area, nbs), self)
                
            elif result == EditAttrDialog.OK:
                oldarea = copy.copy(area)
                nbs = self.get_area_neighbours(area.name)

                attrs = ead.get_attrs()

                newarea = copy.copy(area)
                newarea.name = attrs['name']
                del attrs['name']
                newarea.attrs = copy.copy(attrs)
                
                self.gameconf.ur_stack.push_action(Action.ChangeAreaAction(oldarea, newarea, nbs), self)
            ead.destroy()
         
    def add_area(self, area, neighbours):
        self.gameconf.get_area_info().add_area(area, neighbours)
        self.draw_area(area.name)

    def undraw_area(self, area_name):
        self.undraw_area_text(area_name)
        self.undraw_area_poly_handle(area_name)
        cid = self.area_cid[area_name]
        self.unhighlight_area(cid)
        self.map_canvas.delete(cid)
        del self.area_cid[area_name]
        
    def remove_area(self, area_name):
        if self.has_area_name(area_name):
            area_cid = self.area_cid[area_name]

            # remove current item if needed
            if self.current_item_cid == area_cid:
                self.unset_current_item_cid()

            # remove from map
            self.undraw_area(area_name)
            
            # remove from AreaInfo
            self.gameconf.get_area_info().remove_area(area_name)

    def bind_editor_keys(self):
        if self.editor_keys is not None:
            for event, cmd, _ in self.editor_keys:
                if cmd is not None:
                    self.map_canvas.bind(event, cmd)

    def remove_editor_bindings(self, keys):
        for event, cmd, _ in keys:
            if cmd is not None:
                self.map_canvas.unbind(event)
        
    def delete_areas(self):
        for area_name in self.area_cid.keys():
            self.remove_area(area_name)

    def stop_edit_areas_mode(self):
        self.undraw_poly()
        self.remove_editor_bindings(self.area_editor_keys)
        self.hide_area_editor()
        self.unset_current_item_cid()
    
    def change_mode(self, new_mode, show_func, keys, stop_func):
        if self.mode == new_mode:
            return

        exec_if_not_None(self.stop_editor_func)
        self.mode = new_mode
        self.show_editor_func = show_func
        self.stop_editor_func = stop_func
        self.editor_keys = keys
        exec_if_not_None(show_func)
        self.bind_editor_keys()
    
    def get_canvas_point(self, x, y):
        x = self.map_canvas.canvasx(x)
        y = self.map_canvas.canvasy(y)
        return x, y

    def draw_circle(self, x, y, radius, outline, colour, tags):
        return self.map_canvas.create_oval(
            x-radius,
            y-radius,
            x+radius,
            y+radius,
            outline = outline,
            fill = colour,
            tags = tags)

    def draw_poly(self):
        if len(self.cur_poly) == 0:
            return
        p0 = self.cur_poly[0]
        x, y = self.compute_cvs_xy(p0)
        self.draw_circle(x, y, 5, "black", "red", ("newpoly",))
        
        #if len(self.cur_poly) > 1:

        for p1 in self.cur_poly[1:]:
            x, y = self.compute_cvs_xy(p1)
            self.draw_circle(x, y, 5, "black", "white", "newpoly")

            p0_cvs = self.compute_cvs_xy(p0)
            p1_cvs = self.compute_cvs_xy(p1)
            l = apply(
                Canvas.Line,
                (self.map_canvas,) + p0_cvs + p1_cvs)
            l['tags'] = ("newpoly",)
            l['fill'] = "white"
            l['width'] = 3
            p0 = p1
        if len(self.cur_poly) > 2:
            p3_cvs = self.compute_cvs_xy(self.cur_poly[0])
            l = apply(
                Canvas.Line,
                (self.map_canvas,) + p1_cvs + p3_cvs)
            l['tags'] = ("newpoly",)
            l['fill'] = "white"
            l['width'] = 1


    def define_next_point(self, e):
        self.unset_current_item_cid()
        point = self.compute_real_xy(self.get_canvas_point(e.x, e.y))
        if self.cur_poly == [] or self.cur_poly[-1] != point:
            self.gameconf.ur_stack.push_action(Action.DefineNextAreaPointAction(point), self)

    def define_last_point(self, e):
        if len(self.cur_poly) > 1:
            area_name = self.gameconf.get_area_info().get_new_area_name()
            fg, bg = self.get_current_colour()
            attrs = {
                     "name" : area_name,
                     "polygon" : self.cur_poly[:],
                     Area.FGCOLOUR : fg,
                     Area.BGCOLOUR: bg
                     }
            area, neighbours = self.gameconf.get_area_info().make_area(attrs)
            
            if area is not None:
                self.gameconf.ur_stack.push_action(
                    Action.DefineLastAreaPointAction(area, neighbours),
                    self)
            else:
                guiutils.display_error_message(
                    "Could not create area %s" % area_name)

    def remove_current_area(self, e):
        if self.is_area_cid(self.current_item_cid):
            area_name = self.area_cid.inv[self.current_item_cid]
            area = self.get_area_for_name(area_name)
            nbs = self.get_area_neighbours(area_name)
            self.gameconf.ur_stack.push_action(Action.RemoveAreaAction(area, nbs), self)
        
    def open_save_areas_dialog(self):
        filename = io.open_save_dialog(
            "",
            gameconf.DEFAULT_AREAS_FILENAME,
            "Area file",
            title="Save areas to separate file")
        if filename:
            if not self.gameconf.get_area_info().save_areas(filename):
                guiutils.display_error_message("Could not write to file %s" % filename)

    def undraw_areas(self):
        self.current_area_cid = None
        for aname in self.gameconf.get_area_info().areas:
            self.undraw_area(aname)

    def draw_areas(self):
        for aname in self.gameconf.get_area_info().areas:
            self.draw_area(aname)
        if self.mode != EDIT_AREAS_MODE:
            self.hide_areas()
        
    def load_areas(self, filename):
        if self.gameconf.get_area_info().load_areas(filename):
            self.adjust_map_canvas()
        else:
            guiutils.display_error_message("Could not open areas file %s" % filename)
            
    def load_and_draw_areas(self, filename):
        self.load_areas(filename)
        self.draw_areas()
            
    def open_load_areas_dialog(self):
        filename = io.open_load_dialog(
            "",
            gameconf.DEFAULT_AREAS_FILENAME,
            "Area files",
            title="Load areas file")
        if filename:
            self.load_and_draw_areas(filename)

    def compute_cvs_xy(self, xy):
        '''
        Computes the canvas coordinates of the real (i.e., game)
        coordinates xy. xy is a tuple containing two integers.
        '''
        sf = self.scale_factor
        return int(0.5 + xy[0]*sf), int(0.5 + xy[1]*sf)
    
    def compute_real_xy(self, xy):
        '''
        Computes the real (i.e., game) coordinates of the canvas
        coordinates xy. xy is a tuple containing two integers.
        '''
        sf = self.scale_factor
        return int(0.5 + xy[0]/sf), int(0.5 + xy[1]/sf)
    
    def get_map_center_pos(self):
        x = self.map_canvas.winfo_width()/2.0
        y = self.map_canvas.winfo_height()/2.0
        x0, y0 = self.compute_real_xy(self.get_canvas_point(x, y))
        return x0, y0
    
    def zoom(self, sign):
        '''
        Zoom in/out of the map, depending on value of sign. Sign shall
        be 1.0, 0.0, or -1.0. If sign is positive, this method zooms in on the map. 
        If sign is zero, this method undoes the current zooming (if any).
        If sign is negative, this method zooms out of the map.
        This method first changes self.scale_factor with +/- SCALESTEP (depending
        on sign). Then the map and the area outlines are scaled by 
        self.scale_factor w.r.t. the original size (which is
        not always the current size).
        The center of  the scrollregion on the canvas remains in the center.
        '''
        
        # compute in sf the new scale_factor
        sf = self.scale_factor + sign * SCALESTEP
        if sign==0:
            if self.scale_factor==1.0:
                return
            else:
                sf = 1.0
        # store current center point
        x0, y0 = self.get_map_center_pos()
        
        
        self.undraw_game()
        # set new scale_factor and perform scaling
        self.scale_factor = sf
        #self.scale_map(self.scale_factor)
        self.draw_game()
        if self.mode == EDIT_AREAS_MODE:
            self.show_areas()
        cx, cy = self.compute_cvs_xy((x0, y0))
        self.map_canvas.xview_moveto(cx)
        self.map_canvas.yview_moveto(cy)
        
    def zoom_in(self, args):
        self.zoom(1.0)
        
    def zoom_out(self, args):
        if self.scale_factor > SCALESTEP:
            self.zoom(-1.0)

    def zoom_null(self, args):
        self.zoom(0.0)

    def has_map(self):
        return self.gameconf.has_map()

    def map_bbox(self):
        if self.game_map_tk is None:
            bbox = (0, 0, 0, 0)
        else:
            bbox =  (0, 0, self.scale_factor*self.game_map_tk.width(), self.scale_factor*self.game_map_tk.height())
        return bbox

    def undraw_map(self):
        self.hide_map()
        self.game_map_tk = None
            

    def draw_map(self):
        if self.gameconf.has_map():
            photoimage = self.make_tk_photoimage(self.gameconf.map_reader.get_image_data())
            #self.game_map_tk = scalecopy_tk_photoimage(
            #    photoimage, 
            #    1.0)#self.scale_factor)
            self.game_map_tk = photoimage
            self.show_map()
        else:
            self.map_image_cid = None
        self.adjust_map_canvas()
        if self.scale_factor != 1.0:
            self.hide_map()

    def make_tk_photoimage(self, image_data):
        tmp_file = tempfile.NamedTemporaryFile(prefix = "irtmp",
                                               delete = False)
        tmp_file.write(image_data)
        #uncomment for debug:
        #print tmp_file.name
        tmp_file.close()
        photoimage = None
        try:
            photoimage = Tkinter.PhotoImage(file = tmp_file.name)
        except (Exception, ), e:
            sys.stderr.write(
                "Error: could not create temporary file:\n%s" % str(e))
        os.remove(tmp_file.name)
        return photoimage
    
    def hide_map(self):
        self.map_canvas.delete(self.map_image_cid)
        self.map_image_cid = None

    def show_map(self):
        tags = (MAP_TAG, SCALEABLE)
        self.map_image_cid = self.map_canvas.create_image(
            0, 0, anchor="nw", image=self.game_map_tk, tags = tags)
        self.map_canvas.lower(MAP_TAG, CanvasUnit.UNIT_TAG)
        self.correct_z_order()

    def correct_z_order(self):
        if self.mode == EDIT_AREAS_MODE:
            self.map_canvas.lower(MAP_TAG, AREA_TAG)
        else:
            self.map_canvas.lower(MAP_TAG, AREA_TEXT_TAG)
            self.map_canvas.lower(AREA_TAG, MAP_TAG)

        
    def toggle_map(self, dummyargs=None):
        if self.map_image_cid is None:
            self.show_map()
        else:
            self.hide_map()

    def adjust_map_canvas(self):
        '''
        Recomputes the size of self.map_canvas["scrollregion"]:
        - if there is a map, this determines the size,
        - if there is no map, but there are areas, the scrollregion
          will be just large enough to contain all areas,
        - if there is no map and no areas, the scrollregion
          will not be changed.
        '''

        area_bbox = tuple([c*self.scale_factor for c in self.gameconf.get_area_info().get_areas_bbox()])
        map_bbox = self.map_bbox()

        bbox = add_bboxes(area_bbox, map_bbox)
        if bbox == (0, 0, 0, 0):
            f=0.50# initial canvas size as a fraction of the screen size
            bbox = (0, 
                    0, 
                    int(f * self.maingrid.winfo_screenwidth()),
                    int(f * self.maingrid.winfo_screenheight()))
        if bbox != (0,0,0,0):
            if self.map_canvas["scrollregion"] != bbox:
                self.map_canvas["scrollregion"] = bbox

    def load_map(self, new_map_filename):
        game_dir = self.game_dir
        if game_dir is None:
            game_dir = os.curdir
        new_map_bytes = ""
        self.push_cursor("watch")
        if not os.path.isabs(new_map_filename):
            new_map_filename = os.path.join(game_dir, new_map_filename)
        try:
            img_file = open(new_map_filename, "rb")
            new_map_bytes = img_file.read()
            img_file.close()
        except (IOError), e:
            guiutils.display_error_message("Could not load map %s" % new_map_filename, e)
            return False
        self.pop_cursor()

        map_ext = os.path.splitext(os.path.basename(new_map_filename))[1]
        new_map_filename = "map" + map_ext

        old_map_bytes = self.gameconf.get_map_img_data()
        old_map_filename = self.gameconf.get_map_filename()

        self.gameconf.ur_stack.push_action(Action.ReplaceMapAction(
            old_map_filename,
            old_map_bytes,
            new_map_filename,
            new_map_bytes), self)
        return True

    def open_load_map_dialog(self):
        filename = io.open_load_dialog(
            self.gameconf.get_map_filename(),
            gameconf.DEFAULT_MAP_FILENAME,
            "Map files",
            title="Load map")
        if len(filename)>0:
            self.load_map(filename)
                    
    ##################################################################
    # setup functions
    ##################################################################

    def undraw_unit(self, unit):
        '''
        Removes the unit from the game board and updates
        - unit_order list
        - stack numbers
        accordingly.
        '''
        ucid = self.unit_cid(unit)
        if ucid is None:
            return
        self.canvas_unit[unit].undraw()
        del self.canvas_unit[unit]
        self.unit_cids.discard(ucid)

    def put_unit_on_canvas(self, unit, cx, cy, tags):
        pic_cid = self.map_canvas.create_image(
            cx,
            cy,
            image = self.get_tk_bitmap(unit),
            tags = tags)
        self.unit_cids.add(pic_cid)
        return pic_cid
    
    def remove_unit_from_canvas(self, unit):
        pic_cid = self.unit_cid(unit)
        self.unit_cids.remove(pic_cid)
        self.map_canvas.delete(utils.tag_for_name(unit.name))
        
    def draw_unit(self, unit):
        if self.is_visible(unit):
            if self.canvas_unit.get(unit, None) is None:
                self.canvas_unit[unit] = CanvasUnit.CanvasUnit(self, unit) 
            self.canvas_unit[unit].draw()
            self.update_unit_order()
            pic_cid = self.canvas_unit[unit].pic_cid
            self.unit_cids.add(pic_cid)

        
    def undraw_setup(self):
        self.current_item_cid = None
        for u in self.gameconf.get_units():
            self.undraw_unit(u)
        self.set_current_item_cid(None)

    def is_spy(self, u):
        return u.utypename == "Spy"

    def is_submerged_vessel(self, u):
        return u.attrs.get("submerged", "no") == "yes"

    def is_destroyer(self, u):
        return u.utypename == "Destroyer"
    
    def detects_subs(self, u):
        return self.is_destroyer(u)
        
    def is_land(self, area):
        return area.attrs.get("type", "unknown area type") == "land"

    def is_visible(self, unit):
        # TODO: to correct after players are implemented
        return True
    
    def draw_setup(self):
        self.make_colour_buttons()
        for unit in self.gameconf.get_units():
            self.draw_unit(unit)
        self.refresh_report()

    def load_setup(self, filename):
        self.gameconf.load_setup(filename)
        
    def load_and_draw_setup(self, filename):
        self.load_setup(filename)
        self.draw_setup()
        
    def open_load_setup_dialog(self):
        filename = io.open_load_dialog(
            "",
            gameconf.DEFAULT_SETUP_FILENAME,
            "Setup files",
            title="Load setup file")
        if filename:
            self.load_and_draw_setup(filename)
        
    def open_save_setup_dialog(self):
        filename = io.open_save_dialog(
            "",
            gameconf.DEFAULT_SETUP_FILENAME,
            "Setup files",
            title="Save unit setup to separate file")
        if filename:
            self.save_setup(filename)

    def save_setup(self, filename):
        try:
            outputfile = file(filename, 'w')
            outputfile.write(self.gameconf.get_setup_file_contents())
            outputfile.close()
        except IOError:
            guiutils.display_error_message("Could not save setup to %s" %
                                  filename)
            return False
        return True
        
    def edit_areas_mode(self, dummyargs=None):
        self.change_mode(
            EDIT_AREAS_MODE,
            self.show_area_editor,
            self.area_editor_keys,
            self.stop_edit_areas_mode)
        
    def edit_setup_mode(self, dummyargs=None):
        self.change_mode(
            SETUP_MODE,
            self.show_setup_editor,
            self.setup_editor_keys,
            self.stop_setup_mode)

    def edit_colouring_mode(self, dummyargs=None):
        self.change_mode(
            COLOURING_MODE,
            self.show_colouring_editor,
            self.colouring_editor_keys,
            self.stop_colouring_mode)

    def colour_pick_mode(self, dummyargs=None):
        self.change_mode(
            COLOUR_PICK_MODE,
            self.show_colour_pick_editor,
            self.colour_pick_keys,
            self.stop_colour_pick_mode)

    def stop_setup_mode(self):
        if self.setup_editor is not None:
            self.remove_editor_bindings(self.setup_editor_keys)
            self.hide_setup_editor()
        self.unset_current_item_cid()

    def stop_colouring_mode(self):
        if self.colouring_editor is not None:
            self.remove_editor_bindings(self.colouring_editor_keys)
            self.hide_colouring_editor()
        self.unset_current_item_cid()

    def stop_colour_pick_mode(self):
        if self.colour_pick_editor is not None:
            self.remove_editor_bindings(self.colour_pick_keys)
            self.hide_colour_pick_editor()
        
    def remove_setup_editor(self):
        if self.setup_editor is not None:
            if self.stop_editor_func == self.stop_setup_mode:
                self.stop_editor_func()
            self.unit_lf.destroy()
            self.setup_editor.destroy()
            self.setup_editor = None

    def create_setup_editor(self):
        self.setup_editor = Tkinter.Frame(
            self.edit_panel,
            bd=FG_BORDER_WIDTH)

        # Player panel
        self.setup_editor.player_var = Tkinter.StringVar()
        self.setup_editor.player_var.set("")
        self.setup_editor.player_panel = Tkinter.LabelFrame(
            self.setup_editor,
            text = "Player")
        self.make_player_panel()
        self.setup_editor.player_panel.pack(
            side=Tkinter.TOP,
            expand=False,
            fill=Tkinter.X,
            anchor=Tkinter.NW)

        # Unit buttons
        self.setup_editor.unit_buttons_panel = Tkinter.LabelFrame(
            self.setup_editor, 
            text = "Units")
        self.make_colour_buttons()        
        self.unit_lf = None
        self.current_unit_var = Tkinter.StringVar()
        self.current_unit_var.set("")#self.gameconf.utypes.keys()[0])
        self.make_unit_panel()
        self.setup_editor.unit_buttons_panel.pack(
            side=Tkinter.TOP,
            fill = Tkinter.X, 
            expand=False)

    def create_report_text(self):
        self.report_text = ScrolledText.ScrolledText(
            self.maingrid,
            width=40,
            height=20,
            background="ghostwhite")
        self.report_text['font'] = smaller_font(self.report_text['font'], 2)
        self.report_text.grid(
            row=1,
            rowspan=3,
            column=3,
            sticky=Tkinter.NSEW)
        
    def refresh_report(self):
        if not hasattr(self, "report_text"):
            return
        self.report_text.delete("1.0",Tkinter.END)
        self.map_canvas.delete(COMPARE_UNIT_TAG)
        areas = self.gameconf.get_area_info().areas
        anames = areas.keys()
        anames.sort()
        sep = ""
        indent = ""
        self.compute_units_in_area()
        for an in anames:
            
            def focus_on_area(e, area=areas[an]):
                area_cid = self.area_cid[area.name]
                self.center_view_on_area(area)
                self.select_area_cid(area_cid)
            atag = utils.tag_for_name(an)
            self.report_text.insert(Tkinter.END, sep + indent + an, atag)
            sep = "\n"
            self.report_text.tag_bind(
                atag,
                "<Button-1>", 
                focus_on_area)
            units = self.get_units_in_area(an)
            for u in units:
                indent = "    "
                if u.nr_move_dests()>0:
                    indent = " >> "
                utag = utils.tag_for_name(u.name)
                self.report_text.insert(Tkinter.END, sep + indent, utag)
                def focus_on_unit(e=None, u=u):
                    self.canvas_unit[u].raise_to_top()
                    self.center_view_on_unit(u)
                    self.set_current_item_cid(self.unit_cid(u))
                index1 = self.report_text.index(Tkinter.END)
                if self.get_tk_bitmap(u) is not None:
                    button = Tkinter.Button(self.report_text, image=self.get_tk_bitmap(u), command=focus_on_unit)
                    tt(button, text="%s\n\n%s" % (u.name, u.get_attrs_str()))
                    self.report_text.window_create(Tkinter.END, window=button)
                unit_info = "%s [A%s/D%s/M%s]" % (u.name, 
                                                  u.attrs.get("Att", "-"),
                                                  u.attrs.get("Def", "-"),
                                                  u.attrs.get("Mve", "-"))
                self.report_text.insert(Tkinter.END, unit_info, utag)
                index2 = self.report_text.index(Tkinter.END)
                self.report_text.tag_add(utag, index1, index2)
                self.report_text.tag_bind(
                    utag,
                    "<Button-1>",
                    focus_on_unit)
            indent = ""
        self.center_report_on_current_item()

    def toggle_unit_list(self, e):
        self.show_unit_list.set(not self.show_unit_list.get())
        self.update_unit_list()
        
    def update_unit_list(self):
        if self.show_unit_list.get():
            self.show_report()
        else:
            self.hide_report()
            
    def hide_report(self):
        if hasattr(self, "report_text"):
            self.report_text.grid_forget()
        
    def show_report(self):
        self.create_report_text()
        self.refresh_report()
    
    def open_compare_game_dialog(self, dummyargs=None):
        filename = io.open_load_dialog(
            "",
            gameconf.DEFAULT_GAME_FILENAME,
            "Game files",
            title="Compare current game to game file")
        if filename:
            self.compare_with_game(filename)

    def compare_with_game(self, filename):
        conf,errors = gameconf.load_gameconf_from_archive(filename)
        if len(errors)>0:
            guiutils.display_error_message("\n".join(errors))
            return
        new_units = [u for u in self.gameconf.units.values() if u.name not in conf.units]
        new_units.sort()
        old_units = [u for u in conf.units.values() if u.name not in self.gameconf.units]
        old_units.sort()
        
        def cmpunits(u1, u2):
            result = u1.attr_cmp(u2, exclude=["location"])
            return result
        changed_units = []
        for uname1 in self.gameconf.units:
            if uname1 in conf.units:
                u1 = self.gameconf.units[uname1]
                u2 = conf.units[uname1]
                d = cmpunits(u1, u2)
                if len(d)>0:
                    u1.diff_with_unit = [u2,d]
                    changed_units.append(u1)
        changed_units.sort()
        
        # ensure the unit list is visible
        if not self.show_unit_list.get():
            self.toggle_unit_list(None)
            
        self.report_text.delete("1.0",Tkinter.END)
        self.map_canvas.delete(COMPARE_UNIT_TAG)
        sep = ""
        diffs = 0
        if self.gameconf.get_map_img_data() != conf.get_map_img_data():
            diffs += 1
            self.report_text.insert(Tkinter.END, "Map has changed")
            sep = "\n"
            
        if len(new_units)>0:
            diffs += 1
            self.report_text.insert(Tkinter.END, "%s%d new units" % (sep, len(new_units)))
            sep = "\n"
            for u in new_units:
                indent = "    "
                if u.nr_move_dests()>0:
                    indent = " >> "
                utag = utils.tag_for_name(u.name)
                self.report_text.insert(Tkinter.END, sep + indent, utag)
                def focus_on_unit(e=None, u=u):
                    self.canvas_unit[u].raise_to_top()
                    self.center_view_on_unit(u)
                    self.set_current_item_cid(self.unit_cid(u))
                index1 = self.report_text.index(Tkinter.END)
                if self.get_tk_bitmap(u) is not None:
                    button = Tkinter.Button(self.report_text, image=self.get_tk_bitmap(u), command=focus_on_unit)
                    tt(button, text="%s\n\n%s" % (u.name, u.get_attrs_str()))                    
                    self.report_text.window_create(Tkinter.END, window=button)
                unit_info = "%s [A%s/D%s/M%s]" % (u.name, 
                                                  u.attrs.get("Att", "-"),
                                                  u.attrs.get("Def", "-"),
                                                  u.attrs.get("Mve", "-"))
                self.report_text.insert(Tkinter.END, unit_info, utag)
                index2 = self.report_text.index(Tkinter.END)
                self.report_text.tag_add(utag, index1, index2)
                self.report_text.tag_bind(
                    utag,
                    "<Button-1>",
                    focus_on_unit)
            indent = ""

        
        if len(old_units)>0:
            diffs += 1
            self.report_text.insert(Tkinter.END, "%s%d old units" % (sep, len(old_units)))
            sep = "\n"
            for u in old_units:
                self.canvas_unit[u].pic_cid = None
                u.visible = Tkinter.IntVar()
                u.visible.set(0)
                indent = "    "
                if u.nr_move_dests()>0:
                    indent = " >> "
                utag = utils.tag_for_name(u.name)
                self.report_text.insert(Tkinter.END, sep + indent, utag)
                def show_hide_old_unit(e=None, u=u, utag=utag):
                    if u.visible.get() == 0:
                        
                        # TODO: check if always false !
                        if self.unit_cid(u) is not None:
                            self.canvas_unit[u].undraw()
                    else:
                        cu = CanvasUnit.CanvasUnit(self, u, unittag=COMPARE_UNIT_TAG, bindevents=False)
                        cu.draw()
                        self.center_view_on_unit(u)
                index1 = self.report_text.index(Tkinter.END)
                button = Tkinter.Checkbutton(self.report_text, 
                                             image=self.get_tk_bitmap(u), 
                                             command=show_hide_old_unit,
                                             variable = u.visible)
                self.report_text.window_create(Tkinter.END, window=button)
                unit_info = "%s [A%s/D%s/M%s]" % (u.name, 
                                                  u.attrs.get("Att", "-"),
                                                  u.attrs.get("Def", "-"),
                                                  u.attrs.get("Mve", "-"))
                self.report_text.insert(Tkinter.END, unit_info, utag)
                index2 = self.report_text.index(Tkinter.END)
                self.report_text.tag_add(utag, index1, index2)
                self.report_text.tag_bind(
                    utag,
                    "<Button-1>",
                    show_hide_old_unit)
            indent = ""

        if len(changed_units)>0:
            diffs += 1
            self.report_text.insert(Tkinter.END, "%s%d changed units" % (sep, len(changed_units)))
            sep = "\n"
            for u in changed_units:
                indent = "    "
                if u.nr_move_dests()>0:
                    indent = " >> "
                utag = utils.tag_for_name(u.name)
                self.report_text.insert(Tkinter.END, sep + indent, utag)
                def focus_on_unit(e=None, u=u):
                    self.canvas_unit[u].raise_to_top()
                    self.center_view_on_unit(u)
                    self.set_current_item_cid(self.unit_cid(u))
                index1 = self.report_text.index(Tkinter.END)
                if self.get_tk_bitmap(u) is not None:
                    button = Tkinter.Button(self.report_text, image=self.get_tk_bitmap(u), command=focus_on_unit)
                    tt(button, text="%s\n\n%s" % (u.name, u.get_attrs_str()))
                    self.report_text.window_create(Tkinter.END, window=button)
                unit_info = "%s [A%s/D%s/M%s]" % (u.name, 
                                                  u.attrs.get("Att", "-"),
                                                  u.attrs.get("Def", "-"),
                                                  u.attrs.get("Mve", "-"))
                self.report_text.insert(Tkinter.END, unit_info, utag)
                index2 = self.report_text.index(Tkinter.END)
                self.report_text.tag_add(utag, index1, index2)
                self.report_text.tag_bind(
                    utag,
                    "<Button-1>",
                    focus_on_unit)
                indent += "    "
                for da in u.diff_with_unit[1]:
                    newvalue,oldvalue = u.diff_with_unit[1][da]
                    self.report_text.insert(Tkinter.END, 
                                            "\n%s%s: [%s] was [%s]" % (indent, da, newvalue, oldvalue))
            indent = ""
        
        if diffs == 0:
            self.report_text.insert(Tkinter.END, "No differences found.")

        
    def filter_text(self, k, v, i):
        self.buffer.append(v)
        
    # TODO: maybe this should be moved to CanvasUnit
    def delete_key(self, event):
        '''
        Deletes the selected unit:
        - if it is a moving unit, the move is deleted (not the unit)
        - if it is a unit without move, the unit is deleted.
        '''
        if self.current_item_cid is not None:
            unit = self.get_unit_for_cid(self.current_item_cid)
            if unit is not None:
                self.gameconf.ur_stack.push_action(Action.DeleteUnitAction(unit), self)

    def rgb256_colour(self, colour):
        cbits = TKPHOTOIMAGE_DEPTH - 8
        result = tuple([ c >> cbits for c in self.tkroot.winfo_rgb(colour)])
        return result

    def colour_transform(self, colour, factor):
        result = "#"
        for e in self.rgb256_colour(colour):
            if e==0: e=1
            r = (e*factor)/256
            if r<0: r = 0
            if r>255: r = 255
            result += ("%02x" % r)
        return result

    def colour_fade(self, c1 , f, c2):
        '''
        Returns a colour with rgb values between the
        rgb values of colours c1 and c2. The percentage f denotes
        where the result lies on a linear scale between c1 and c2:
        f=0.0 (or smaller) means the new colour is equal to c1, f=0.5 means the
        new colour is halfway between c1 and c2, f=1.0 (or lager) means
        the new colour is equal to c2.
        '''
        if f<0.0:
            f = 0.0
        if f>1.0:
            f = 1.0
        rgb1 = self.rgb256_colour(c1)
        rgb2 = self.rgb256_colour(c2)
        rgb = []
        for i in [0,1,2]:
            rgb.append(rgb1[i]*(1.0-f) + rgb2[i]*f)
        result = rgb256_str(tuple(rgb))
        return result

    def get_map_colour_at(self, x, y):
        return tuple(map(int, self.game_map_tk.get(x, y).split()))
        
    def fill_photoimage_area(self, phimg, x, y, new_colour):
        queue = [(x,y)]
        
        # line_segment stores for each y coordinate the x-coordinates
        # of line segments inside the area.
        # line_segment[u] is an ordered list of x-coordinates.
        line_segment = {}
        
        def add_point(x, y, ls=line_segment):
            if y not in ls:
                ls[y] = []
            bisect.insort(ls[y], x)
        
        def new_point(x, y, ls=line_segment):
            if y in ls:
                xs = ls[y]
                if len(xs) == 0:
                    return True
                i = bisect.bisect_left(xs, x)
                return (i==len(xs)) or (xs[i] != x)
            return True
        
        def draw_line_segment(x, y, count, colour):
            phimg.put("{" + " ".join([rgb256_str(colour)] * count) + "}", to=(x, y))
            
        x1, y1 = phimg.width()-1, phimg.height()-1
        black = self.rgb256_colour("black")
        while len(queue) > 0:
            x, y = queue[0]
            del queue[0]
            if graphics.in_bbox(x, y, 0, 0, x1, y1):
                cur_colour = map(int, phimg.get(x, y).split())
                if (not same_colour(cur_colour, black, 10)):
                    if new_point(x, y):
                        add_point(x, y)
                        queue.extend([(x-1,y-1), (x-1,y), (x-1,y+1),
                                      (x,y-1), (x,y+1),
                                      (x+1,y-1), (x+1,y), (x+1,y+1)])

        for y in line_segment:
            xs = line_segment[y]
            x0 = xs[0]
            count = 1
            for x1 in xs[1:]:
                if x1 == (x0 + count):
                    count += 1
                else:
                    draw_line_segment(x0, y, count, new_colour)
                    x0 = x1
                    count = 1
            draw_line_segment(x0, y, count, new_colour)
        self.map_canvas.update_idletasks()
        img_data = self.get_img_data_from_photoimage(phimg)
        self.gameconf.map_reader.set_image_data(img_data)
    
    def get_img_data_from_photoimage(self, phimg):
        tmp_file = tempfile.NamedTemporaryFile(prefix = "irtmp",
                                               delete = False)
        tmp_file.close()
        phimg.write(tmp_file.name)
        
        with open(tmp_file.name) as imgfile:
            image_data = imgfile.read()
        os.remove(tmp_file.name)
        return image_data
    
    def get_current_unit_typename(self):
        return self.current_unit_var.get()

    def get_current_colour(self):
        return tuple(self.current_colour_var.get().split(":"))

    def set_current_colour(self, fg, bg):
        self.current_colour_var.set(":".join((fg,bg)))

    def get_player(self, pname):
        ps = [ p for p in self.gameconf.players if p.name == pname ]
        if len(ps)>0:
            return ps[0]
        return None

    def get_current_player_name(self):
        return self.setup_editor.player_var.get()

    def get_current_player(self):
        return self.get_player(self.get_current_player_name())

    def get_current_unit_typename_and_fgbg(self):
        utp = self.get_current_unit_typename()
        fgbg = self.get_current_colour()
        if utp is not None and fgbg is not None:
            return utp, fgbg
        return None

    def set_colour_button_colours(self, btn, fg, bg):
        btn.configure(
            foreground=bg,
            selectcolor=fg,
            activeforeground=bg,
            activebackground=fg,
            background=fg,
            value=":".join((fg,bg)),
            )
        btn.panel.configure(background=bg)

    def colour_button_panel(self, parent, row, col):
        '''
        Creates a panel with a colour button. The panel gets the 
        background colour for the colour button.
        '''
        panel = Tkinter.Frame(parent)
        btn = Tkinter.Radiobutton(panel, width=2)
        btn.panel = panel
        btn.pack(side=Tkinter.LEFT, padx=5)
        panel.grid(row=row, column=col, sticky=Tkinter.NSEW)
        return btn
    
    def add_colour_button(self, fg, bg, row, col):
        crbutton = self.colour_button_panel(self.cb_panel, row, col)
        crbutton.configure(
            indicator=0,
            variable=self.current_colour_var,
            command=self.colour_button_pressed
            )
        crbutton.bind('<Button-3>', self.edit_colour)
        self.set_colour_button_colours(crbutton, fg, bg)
        tt(crbutton, text="foreground: %s\nbackground: %s" %(fg, bg))

    def make_colour_buttons(self):
        if self.cb_panel is not None:
            self.cb_panel.destroy()
        self.cb_panel = Tkinter.LabelFrame(self.setup_editor, text="Colours")

        row = 0
        col = 0
        numcol = 6
        for fg, bg in self.gameconf.colours:
            if col>=numcol:
                col = 0
                row += 1
            self.add_colour_button(fg, bg, row, col)
            col += 1

        new_colour_button = Tkinter.Button(
            self.cb_panel,
            text = "New colour",
            command = self.edit_new_colour)
        tt(new_colour_button, text="Define a new colour")
        row += 1
        col = 0
        new_colour_button.grid(row=row, column=col, columnspan=numcol, sticky=Tkinter.NSEW)
            
        self.cb_panel.pack(
            side=Tkinter.TOP,
            fill = Tkinter.X, 
            expand=False)

    def make_unit_panel(self):
        panel = self.setup_editor.unit_buttons_panel
        panel.configure(bd=FG_BORDER_WIDTH)
        
        new_unit_button = Tkinter.Button(
            panel, 
            text = "New unit",
            command = self.create_new_unit_type)
        new_unit_button.pack(side = Tkinter.TOP, fill = Tkinter.X)

        tt(new_unit_button, 
           text="Define a new unit and add it to the available units.",
           follow_mouse=1)
        
        l = Tkinter.Label(panel, text = "", anchor="w")
        l.pack(side = Tkinter.TOP, fill = Tkinter.X)
        self.make_unit_type_buttons()

    def get_bitmap_data(self, u):
        return self.gameconf.get_unit_bitmap_data(u)

    def reset_tk_bitmap(self, u):
        if hasattr(u, "colour"):
            fg, bg = u.colour, u.bgcolour
        else:
            fg, bg = "black", "gray"
        self.unit_tk_bitmap[u] = Tkinter.BitmapImage(data = self.get_bitmap_data(u))
        self.unit_tk_bitmap[u].configure(foreground=fg, background=bg)
        
    def get_tk_bitmap(self, u):
        if self.unit_tk_bitmap.get(u, None) is None:
            self.reset_tk_bitmap(u)
        return self.unit_tk_bitmap[u]

    def make_unit_type_buttons(self):
        panel = self.setup_editor.unit_buttons_panel
        maxcol = 4
        if self.unit_lf is not None:
            self.unit_lf.destroy()

        self.unit_lf = unit_lf = Tkinter.LabelFrame(
            panel,
            text = "Available units")
        unit_buttons_f = Tkinter.Frame(self.unit_lf)
        unit_buttons_f.pack(side=Tkinter.TOP, fill = Tkinter.X,  expand=True)
    
        self.unit_buttons = []
        row = 0
        col = 0
            
        sorted_unit_types = self.gameconf.get_sorted_unit_types()
        terrains = sorted_unit_types.keys()
        terrains.sort()
        for t in terrains:
            for unittype in sorted_unit_types[t]:
                if col == maxcol:
                    col = 0
                    row += 1
                utpn = unittype.name
                fg,bg = self.get_current_colour()
                button=Tkinter.Radiobutton(
                    unit_buttons_f,
                    #image=unittype.get_tk_bitmap(),
                    image=self.get_tk_bitmap(unittype),
                    indicator = 0, # don't display radio button indicator

                    foreground = fg,
                    background = bg,

                    activeforeground = fg,
                    activebackground = bg,

                    disabledforeground = fg,
                    selectcolor = bg,

                    highlightcolor = fg,
                    highlightbackground = bg,

                    value=utpn,
                    variable=self.current_unit_var,
                    command=self.select_unit_type)
                button.bind('<Button-3>', self.edit_unit_type)
                button.grid(
                    row=row,
                    column=col,
                    sticky=Tkinter.NSEW)
                tt(button,
                   text="%s\n\n%s" % (utpn, unittype.get_attr_str()))
                unit_buttons_f.columnconfigure(col, weight=1)

                col += 1
                self.unit_buttons.append(button)
            col = 0
            row += 1

        cur_unit_f = Tkinter.Frame(self.unit_lf, relief="sunken", borderwidth=5)
        cur_unit_f.pack(side=Tkinter.TOP, fill = Tkinter.BOTH,  expand=True)
  

        cur_unit_f.columnconfigure(0, weight=0)
        cur_unit_f.columnconfigure(1, weight=2)
        row = 0
        num_label = Tkinter.Label(
            cur_unit_f,
            anchor=Tkinter.W,
            text="Units to place"
            )
        tt(num_label, text="Number of units to place on the map (by left-mouse-click)")
        num_label.grid(
            sticky=Tkinter.EW,
            row=row,
            column=0
            )

        self.num_spinbox = Tkinter.Spinbox(
            cur_unit_f,
            from_ = 0,
            width = 1,
            to = 100
            )
        self.num_spinbox.grid(
            sticky=Tkinter.EW,
            row=row,
            column=1)
            
        row += 1
        self.ut_label = ut_label = Tkinter.Label(
            cur_unit_f,
            padx=1,
            anchor=Tkinter.SW,
            text="Unit type"
            )
        tt(self.ut_label, text="Type of unit to place on the map (by left-mouse-click")
        ut_label.grid(
            sticky=Tkinter.EW,
            row=row,
            ipady=2,
            column=0)
        self.ut_val_label = ut_val_label = Tkinter.Label(
            cur_unit_f,
            relief="sunken",
            anchor=Tkinter.SW,
            text="not selected"
            )
        ut_val_label.grid(
            sticky=Tkinter.EW,
            row=row,
            ipady=2,
            column=1)
        
        unit_lf.pack(side = Tkinter.TOP, fill = Tkinter.X,  expand=True)
        return unit_lf

    def num_units_to_place(self):
        return int(self.num_spinbox.get())

    def set_num_units_to_place(self, num):
        self.num_spinbox.delete(0, Tkinter.END)
        self.num_spinbox.insert(0, str(num))

    def change_unit_button_value(self, oldvalue, newvalue):
        for b in self.unit_buttons:
            if b['value'] == oldvalue:
                b['value'] = newvalue
                if self.current_unit_var.get() == oldvalue:
                    self.current_unit_var.set(newvalue)

    def update_unit_button_icon(self, utp):
        for b in self.unit_buttons:
            if b['value'] == utp.name:
                #b['image'] = utp.get_tk_bitmap()
                b['image'] = self.get_tk_bitmap(utp)
        

    def make_player_panel(self):
        panel = self.setup_editor.player_panel
        panel.configure(bd=FG_BORDER_WIDTH)
        r = c = 0

        current_player_name = self.get_current_player_name()

        panel.columnconfigure(c, weight=1)
        player_names = [ p.name for p in self.gameconf.players ]

        if current_player_name not in player_names:
            current_player_name = ""
            #player_names = [current_player_name] + player_names
        self.setup_editor.player_omenu = apply(
            Tkinter.OptionMenu,
            [ self.setup_editor.player_panel,
              self.setup_editor.player_var ] +
            player_names,
            { 'command': self.select_player }
            )
        tt(self.setup_editor.player_omenu, text="Select a player from this list")
        self.setup_editor.player_var.set(current_player_name)        

        self.setup_editor.player_omenu.grid(
            row = r, 
            column = c, 
            sticky = Tkinter.EW)

        # "New" player button
        r, c = r+1, 0
        self.setup_editor.new_player_button = Tkinter.Button(
            panel,
            text="New player",
            command = self.show_player_editor)
        tt(self.setup_editor.new_player_button, text="Add a player")
        self.setup_editor.new_player_button.grid(
            row=r,
            column=c,
            sticky=Tkinter.EW)

        # "New" player button
        r, c = r+1, 0
        self.setup_editor.change_player_button = Tkinter.Button(
            panel,
            text="Change player",
            command = self.change_player)
        tt(self.setup_editor.change_player_button, text="Change the current player's properties")
        self.setup_editor.change_player_button.grid(
            row=r,
            column=c,
            sticky=Tkinter.EW)

        # "change password" button
        r, c = r+1, 0
        self.setup_editor.player_password_button = Tkinter.Button(
            panel,
            text="Change password",
            command=self.change_password)
        tt(self.setup_editor.player_password_button, text="Set or change the current player's password")
        self.setup_editor.player_password_button.grid(
            row=r,
            column=c,
            sticky=Tkinter.EW)

    def select_player(self, pname):
        self.activate_player(self.get_player(pname))

    def change_player(self):
        cp = self.get_current_player()
        if cp is not None:
            self.show_player_editor(player = cp)

    def show_player_editor(self, player=None):
        epd = EditPlayerDialog.EditPlayerDialog(
            self.tkroot,
            height = 300)
        if player is None:
            player = Player.Player("New player")
        epd.set_player(player)
        old_name = player.name
        result = epd.go()
        if result == EditPlayerDialog.CHANGED:
            if player not in self.gameconf.players:
                self.add_player(player)
            elif old_name != player.name:
                self.is_logged_in[player.name] = self.is_logged_in[old_name]
                del self.is_logged_in[old_name]

            self.make_player_panel()
            self.setup_editor.player_var.set(player.name)
            self.activate_player(player)
        elif result == EditPlayerDialog.DELETED:
            if player == self.get_game_master():
                guiutils.display_error_message(
                    "Player '%s' is the game master and cannot be deleted." % 
                    player.name)
            else:
                self.gameconf.players.remove(player)
            self.make_player_panel()
        epd.destroy()

    def colour_button_pressed(self):
        if self.mode == SETUP_MODE:
            self.update_setup_editor()
            
    def update_unit_buttons(self, fg, bg):
        self.setup_editor.configure(
            background=fg)
        self.setup_editor.player_panel.configure(
            background=bg)
        self.setup_editor.unit_buttons_panel.configure(
            background=bg)
        for b in self.unit_buttons:
            b.configure(
                #foreground=fg,
                #background=bg,

                activeforeground = fg,
                activebackground = bg,

                disabledforeground = fg,
                selectcolor = bg,

                highlightcolor = fg,
                highlightbackground = bg,

                )

    def set_state_setup_editor(self, is_authorized):
        if is_authorized:
            cmd = self.change_password
            enabled = True
            msg = "Change password"
        else:
            cmd = self.login_player
            enabled = False
            msg = "Login"
            
        #self.setup_editor.player_name_entry.configure(state=state)
        #self.setup_editor.player_omenu.configure(state = state)
        self.set_enable_setup_editor(enabled)
        self.setup_editor.player_password_button.configure(
            text=msg,
            command=cmd)

    def set_enable_setup_editor(self, enabled):
        state = Tkinter.DISABLED
        if enabled:
            state = Tkinter.NORMAL
        self.setup_editor.player_omenu.configure(state = state)
        for b in self.unit_buttons:
            b.configure(state=state)
        
    def get_game_master(self):
        return self.gameconf.players[0]

    def is_authorized_player(self, player):
        '''
        Returns True if
        - player is logged in, or
        - player does not have a password.
        Returns False otherwise.
        '''
        result = False
        if player is not None:
            if self.is_logged_in[player.name]:
                result = True
            elif not player.has_password():
                result = True
        return result
    
    def authorize_player(self, player):
        if not self.is_authorized_player(player):
            self.login_player(player)
        return self.is_authorized_player(player)

    def change_password(self):
        player = self.get_current_player()
        self.show_change_password_dialog(player, login=True)

    def show_change_password_dialog(self, player, login=False):
        if player is None:
            return
        cpwd = PasswordDialog.ChangePasswordDialog(
            self.tkroot,
            message="Enter new password for \"%s\" twice" % player.name)
        pw = cpwd.go()
        if pw is not None:
            player.set_password(pw)
            msg = "%s's password updated (don't forget to save the game)"
            if login:
                self.is_logged_in[player.name] = True
        else:
            msg = "%s's password not changed"
        guiutils.display_warning_message(msg % player.name)

    def login_player(self, player=None):
        if player is None:
            player = self.get_current_player()
            
        if player is not None:
            pwd = PasswordDialog.PasswordDialog(
                self.tkroot,
                message="Login player %s" % player.name)
            pw = pwd.go()
            self.is_logged_in[player.name] = player.check_password(pw)
            self.update_setup_editor()
     
    def login_gamemaster(self):
        self.login_player(self.get_game_master())

    def activate_player(self, player):
        # Authorize player, if needed, and activate him.
        # player's units and neighbouring units are displayed.
        if self.authorize_player(player):
            if (self.prev_active_player != player):
                if (self.prev_active_player is not None):
                    self.undraw_setup()
                self.draw_setup()
            self.prev_active_player = player
        else:
            guiutils.display_error_message("Incorrect password.")
            prev_name = ""
            if self.prev_active_player is not None:
                prev_name = self.prev_active_player.name
            self.setup_editor.player_var.set(prev_name)
        
    def deactivate_player(self, player):
        # deactivate player.
        self.undraw_setup()
        self.draw_setup()

    def select_unit_type(self):
        self.set_num_units_to_place(1)
        self.ut_val_label.configure(text=self.get_current_unit_typename())

    def create_new_unit_type(self):
        utp = UType.UnitType("New Unit Type", {'bitmap': self.gameconf.get_default_unit_bitmap()})
        self.show_unit_type_editor(utp)
        
    def edit_unit_type(self, args):
        # first select the unit type for the pressed button
        args.widget.invoke()
        # then edit the corresponding unit type
        utpn = self.get_current_unit_typename()
        utp = self.gameconf.get_unit_type(utpn)
        self.show_unit_type_editor(utp)

    def show_unit_type_editor(self, utp):
        eud = EditUnitDialog.EditUnitTypeDialog(
            self.tkroot,
            height = 300,
            is_name_editable = True)
        bitmap_data = self.get_bitmap_data(utp)
        eud.set_unit_type(copy.copy(utp), bitmap_data)
        result = eud.go()
        if result == EditUnitDialog.CANCEL:
            pass
        elif result == EditUnitDialog.DELETED:
            self.gameconf.ur_stack.push_action(Action.DeleteUnitTypeAction(utp), self)
        else:
            bitmap_name = self.gameconf.get_bitmap_name(eud.get_bitmap_data())
            newutp = eud.get_unit_type()
            newutp.attrs['bitmap'] = bitmap_name
            self.gameconf.ur_stack.push_action(Action.ChangeUnitTypeAction(utp, newutp), 
                                      self)
        eud.destroy()
    
    def add_unit_type(self, utp):
        self.unit_tk_bitmap[utp] = None
        self.gameconf.add_unit_type(utp)
        
    def delete_unit_type(self, utp):
        self.gameconf.delete_unit_type(utp.name)
        
    def show_unit_editor(self, unit):
        eud = EditUnitDialog.EditUnitDialog(
            self.tkroot,
            height = 300,
            is_name_editable = False)
        eud.set_unit(copy.copy(unit), self.get_bitmap_data(unit))
        result = eud.go()
        if result == EditUnitDialog.CANCEL:
            pass
        elif result == EditUnitDialog.DELETED:
            self.gameconf.ur_stack.push_action(Action.DeleteUnitAction(unit), self)
        else:
            new_unit = eud.get_unit()
            bitmap_name = self.gameconf.get_bitmap_name(eud.get_bitmap_data())
            new_unit.attrs['bitmap'] = bitmap_name
            if new_unit.name != unit.name:
                guiutils.display_error_message("Unit's name cannot be changed")
            else:
                self.gameconf.ur_stack.push_action(Action.ChangeUnitAction(
                                            unit, 
                                            copy.copy(unit), 
                                            new_unit), 
                                          self)
        eud.destroy()

    def edit_new_colour(self):
        fg, bg = self.show_colour_editor()
        if (fg is not None) and (bg is not None):
            oldfg, oldbg = self.get_current_colour()
            self.gameconf.ur_stack.push_action(Action.AddColourPairAction(
                                        (oldfg, oldbg), 
                                        (fg, bg)), 
                                      self)

    def edit_colour(self, event):
        btn = event.widget
        btn.invoke()
        oldfg, oldbg = btn["background"], btn["foreground"]
        i = self.gameconf.colours.index((oldfg, oldbg))
        if i<0:
            guiutils.display_error_message(
                "Button foregound and background colours are not "
                "in the game configuration!")
            return
        fg, bg = self.show_colour_editor()
        if (fg is not None) and (bg is not None):
            self.gameconf.ur_stack.push_action(Action.ChangeColourPairAction(
                                        (oldfg, oldbg),
                                        (fg, bg)), self)

        elif (fg=="DELETE") and (bg is None):
            self.gameconf.ur_stack.push_action(
                Action.DeleteColourPairAction(
                    self.gameconf.colours.index((oldfg, oldbg)),
                    (oldfg, oldbg)), 
                self)

    def show_colour_editor(self):
        ecd = EditColourDialog.EditColourDialog(self.tkroot)        

        fg,bg = self.get_current_colour()
        ecd.set_colours(fg, bg)
        self.push_cursor("crosshair")
        result = ecd.go()
        self.pop_cursor()
        if result == EditColourDialog.CHANGED:
            fg, bg = (ecd.get_fg(), ecd.get_bg())
        elif result == EditColourDialog.DELETED:
            fg, bg = "DELETE", None
        else:
            fg, bg = None, None
        ecd.destroy()
        return fg, bg

    def left_click_cmd(self, e, shift=False):
        x, y = self.get_canvas_point(e.x, e.y)
        
        if self.num_units_to_place() > 0:
            rx, ry = self.compute_real_xy((x,y))
            num = self.num_units_to_place()
            utpn_fgbg = self.get_current_unit_typename_and_fgbg()
            if  utpn_fgbg is not None:
                utpn, fgbg = utpn_fgbg
                fg, bg = fgbg
                self.place_unit_at(
                    rx,
                    ry,
                    utpn,
                    fg,
                    bg,
                    num = num)
            else:
                guiutils.display_debug_message(
                    "No current unit type and colour selected")
            self.set_num_units_to_place(0)
        if self.find_selected_unit_at(x, y) is None:
            self.find_selected_area_at(x, y)
        self.center_report_on_current_item()
        
    def center_report_on_current_item(self):
        if not self.show_unit_list.get():
            return
        cid = self.current_item_cid
        if cid is not None:
            i = self.get_unit_for_cid(cid)
            if i is None:
                i = self.get_area_for_cid(cid)
            if i is not None:
                tag = utils.tag_for_name(i.name)
                idx = self.report_text.tag_ranges(tag)
                if len(idx)>0:
                    self.report_text.see(idx[0])

    def left_click_colouring_cmd(self, e, shift=False):
        x, y = self.get_canvas_point(e.x, e.y)
        unit_cid = self.find_selected_unit_at(x, y)
        if unit_cid is None:
            area_cid = self.find_selected_area_at(x, y)
            if area_cid is not None:
                rx, ry = self.compute_real_xy((x,y))
                fg,_ = self.get_current_colour()
                self.colour_area(rx, ry, self.rgb256_colour(fg))
        else:
            self.colour_unit(unit_cid)

    def colour_unit(self, unit_cid):
        unit = self.get_unit_for_cid(unit_cid)
        oldfg, oldbg = unit.colour, unit.bgcolour
        newfg, newbg = self.get_current_colour()

        self.gameconf.ur_stack.push_action(Action.ColourUnitAction(
                                    unit.name, 
                                    (oldfg, oldbg),
                                    (newfg, newbg)),
                                  self)
            
    def colour_area(self, x, y, new_colour):
        old_colour = self.get_map_colour_at(x, y)
        if same_colour(old_colour, new_colour):
            return
        self.gameconf.ur_stack.push_action(
            Action.ColourAreaAction(x, y, 
                                    old_colour,
                                    new_colour), self)

    def shift_right_click_cmd(self, e):
        x, y = self.get_canvas_point(e.x, e.y)
        unit_cid = self.find_selected_unit_at(x, y)
        if unit_cid is None:
            self.find_selected_area_at(x, y)
            self.edit_area()

    def left_click_colour_pick(self, e):
        if self.game_map_tk is None:
            guiutils.display_error_message("There is no map to pick a colour from.")
            return
        x, y = self.get_canvas_point(e.x, e.y)
        rx, ry = self.compute_real_xy((x,y))
        colour = map(int, self.game_map_tk.get(rx, ry).split())
        d = Dialog.Dialog(
            None, 
            title= 'Save picked colour',
            text= 'Save the picked colour under a colour button?',
            bitmap = 'questhead',
            default = 0,
            strings = ("Yes", "No"))
        if d.num == 0:
            fg = rgb256_str(tuple(colour))
            oldfg,oldbg = self.get_current_colour()
            bg = oldbg
            self.gameconf.ur_stack.push_action(Action.AddColourPairAction(
                                            (oldfg, oldbg),
                                            (fg, bg)),
                                      self)

    def is_valid_move(self, unit, x, y):
        return True

    def is_new_destination(self, unit, x, y):
        '''
        Checks if the real coordinates (x,y) define a location in another
        area than the current location of the bitmap of unit.
        Returns True if the areas are different, False otherwise.
        '''
        x0, y0 = self.compute_cvs_xy(unit.last_move_xy())
        x1, y1 = self.compute_cvs_xy((x, y))
        cur = self.find_area_cid(x0, y0)
        new = self.find_area_cid(x1, y1)
        return new != cur
    
    def snap_bbox(self, bbox, pred=None):
        '''
        Returns the (x,y) canvas coordinates of a unit which bitmap
        overlaps the given bbox.
        If pred is not None, it shall be a function taking a cid and 
        returning True or False. Only units for which pred(cid) == True
        are considered.
        '''
        if pred is None:
            pred = lambda cid: True
        cids = apply(
            self.map_canvas.find_overlapping,
            bbox)
        cids = [cid for cid in cids
                if pred(cid) and self.is_unit_cid(cid)]
        if len(cids)>0:
            return self.map_canvas.coords(cids[0])
        return graphics.bbox_center(bbox)
    
    def snap_pos(self, xy, pred=None):
        x,y = xy
        return self.snap_bbox((x-1, y-1, x+1, y+1), pred)
    
    def snap_to_other(self, unit):
        '''
        Snap to other returns the canvas coordinates of the unit(s)
        whose bitmap(s) currently overlap the bitmap of the given unit.
        If there are no such units, the canvas coordinates of the given
        unit bitmap are returned.
        '''
        ucid = self.unit_cid(unit)
        unit_bbox = self.map_canvas.bbox(ucid)
        return self.snap_bbox(
            unit_bbox, 
            lambda cid, ucid=ucid: cid != ucid)
            

    def update_stack_number_at(self, cx, cy):
        cids = self.find_unit_cids(cx, cy)
        nr = len(cids)
        tags = ("stacknum-%d-%d" % (cx, cy), UNIT_NUM_TAG)
        self.map_canvas.delete(tags)
        for cid in self.map_canvas.find_withtag((tags[0],)):
            self.map_canvas.delete(cid)
        if nr>1:
            bbox = apply(self.map_canvas.bbox, cids)
            x1, y1 = bbox[2], bbox[3]
            tb = self.map_canvas.create_text(
                x1+5,
                y1,
                fill="black",
                text=str(nr),
                tags=tags)
            tbbox = self.map_canvas.bbox(tb)
            rect = self.map_canvas.create_rectangle(
                tbbox,
                fill="ghostwhite",
                outline="black",
                tags=tags)
            self.map_canvas.tag_raise(rect, cids[0])
            self.map_canvas.tag_raise(tb, rect)
        

    def place_unit_at(self, x, y, utpn, fg, bg, num=1):
        '''
        Creates new units and places them at (x,y). The
        type+colour of the unit are determined by the current selected
        type and colour.
        The param num gives the number of units to place.
        '''

        area_cid = self.area_cid_for_unit_pos(x, y)
        if area_cid is None:
            guiutils.display_error_message("Invalid unit location: no area defined here.")
            return

        unittype = self.gameconf.get_unit_type(utpn)
        new_units = self.make_units(num,
                                    utpn,
                                    fg,
                                    bg,
                                    x,
                                    y,
                                    unittype.attrs,
                                    self.get_current_player_name())
        self.gameconf.ur_stack.push_action(Action.AddUnitsAction(new_units), self)

    def make_units(self, num, utpn, fg, bg, x, y, attrs, player_name):
        new_units = []
        seqn = 0
        while num > 0:
            num -= 1
            unit = None
            seqn = self.gameconf.unique_unit_seq_number(utpn, seqnum=seqn)

            unit = Unit.Unit(
                self.gameconf.make_unit_name(utpn, seqn),
                utpn,
                fg,
                bg,
                x=x,
                y=y)
            seqn += 1
            unit.attrs.update(attrs)
            unit.attrs["player"] = player_name
            new_units.append(unit)
        return new_units
    
    def add_unit(self, unit):
        self.gameconf.add_unit(unit)
        self.unit_tk_bitmap[unit] = None
        self.draw_unit(unit)
        
    def add_units(self, us):
        for u in us:
            self.add_unit(u)

    def delete_unit(self, unit):
        if self.canvas_unit[unit] is not None:
            self.undraw_unit(unit)
        self.gameconf.del_unit(unit)

    def delete_units(self, us):
        for u in us:
            self.delete_unit(u)
            
    def is_bitmap_ext(self, ext):
        return ext == ".xbm"

    def is_pixmap_ext(self, ext):
        return ext in [".ppm", ".gif"]
    
                
    def show_setup_editor(self):
        if self.setup_editor is None:
            self.create_setup_editor()
        self.update_setup_editor()
        self.setup_editor.pack(
            side=Tkinter.TOP,
            fill=Tkinter.Y,
            expand=Tkinter.TRUE,
            anchor=Tkinter.N)

    def show_colouring_editor(self):
        if self.colouring_editor == None:
            self.create_colouring_editor()
        self.colouring_editor.visible = True
        self.colouring_editor.pack(
            side=Tkinter.TOP,
            fill=Tkinter.Y,
            expand=Tkinter.TRUE,
            anchor=Tkinter.N)

    def remove_colouring_editor(self):
        if self.colouring_editor is not None:
            if self.stop_editor_func == self.stop_colouring_mode:
                self.stop_editor_func()
            self.colouring_editor.destroy()
            self.colouring_editor = None

    def create_colouring_editor(self):
        self.colouring_editor = Tkinter.Frame(self.edit_panel)
        self.colouring_editor.visible = False
        self.colouring_editor.info_label = Tkinter.Label(self.colouring_editor)
        self.colouring_editor.info_label.pack(side=Tkinter.TOP,
                                              anchor=Tkinter.N,
                                              fill=Tkinter.BOTH,
                                              expand=Tkinter.TRUE)
        self.colouring_editor.info_label["height"] = 1
        self.colouring_editor.info_label["text"] = "Colouring"

    def show_colour_pick_editor(self):
        if self.colour_pick_editor == None:
            self.create_colour_pick_editor()
        self.push_cursor("crosshair")
        self.colour_pick_editor.visible = True
        self.colour_pick_editor.pack(
            side=Tkinter.TOP,
            fill=Tkinter.Y,
            expand=Tkinter.TRUE,
            anchor=Tkinter.N)

    def remove_colour_pick_editor(self):
        if self.colour_pick_editor is not None:
            if self.stop_editor_func == self.stop_colour_pick_mode:
                self.stop_editor_func()
            self.colour_pick_editor.destroy()
            self.colour_pick_editor = None

    def create_colour_pick_editor(self):
        self.colour_pick_editor = Tkinter.Frame(self.edit_panel)
        self.colour_pick_editor.visible = False
        self.colour_pick_editor.info_label = Tkinter.Label(self.colour_pick_editor)
        self.colour_pick_editor.info_label.pack(side=Tkinter.TOP,
                                              anchor=Tkinter.N,
                                              fill=Tkinter.BOTH,
                                              expand=Tkinter.TRUE)
        self.colour_pick_editor.info_label["height"] = 1
        self.colour_pick_editor.info_label["text"] = "Pick colour"

    def update_setup_editor(self):
        fg,bg = self.get_current_colour()
        self.update_unit_buttons(fg, bg)
        
    def hide_setup_editor(self):
        self.setup_editor.pack_forget()

    def delete_move(self, unit):
        self.canvas_unit[unit].delete_move_arrows()
        self.undraw_unit(unit)
        unit.reset_move_dest()
        self.draw_unit(unit)

    def hide_colouring_editor(self):
        self.colouring_editor.pack_forget()

    def hide_colour_pick_editor(self):
        self.colour_pick_editor.pack_forget()
        self.pop_cursor()

    ##################################################################
    
    def center_view_on_unit(self, unit):
        '''
        Centers the view of the Canvas on the given unit. If this unit
        has movement, an attempt is made to make the whole movement
        visible.
        '''
        x, y = self.compute_cvs_xy((unit.x, unit.y))

        if unit.nr_move_dests()>0:
            # compute point halfway between first and last movement point
            x1, y1 = self.compute_cvs_xy(unit.last_move_xy())
            x = (x + x1) / 2.0
            y = (y + y1) / 2.0
        self.center_view_on_point(x, y)
    
    def center_view_on_area(self, area):
        bbox = self.map_canvas.bbox(self.area_cid[area.name])
        x = (bbox[0] + bbox[2]) / 2
        y = (bbox[1] + bbox[3]) / 2
        self.center_view_on_point(x, y)

    def center_view_on_point(self, x, y, cf=0.10):
        # Move the viewport only if within (cf*100)% of the edge
        # x and y directions are independent of each other
        bbox = self.map_bbox()
        if bbox != (0,0,0,0):
            wd = graphics.bbox_width(bbox)
            ht = graphics.bbox_height(bbox)
            xview_frac = self.map_canvas.xview()
            yview_frac = self.map_canvas.yview()
            xview = (xview_frac[0] * wd, xview_frac[1] * wd)
            yview = (yview_frac[0] * ht, yview_frac[1] * ht)
            view_wd = xview[1] - xview[0]
            view_ht = yview[1] - yview[0]
            if x - cf * view_wd < xview[0] or x + cf * view_wd > xview[1]:
                xf = x - self.map_canvas.winfo_width()/2.0
                xf = float(xf) / float(wd)
                self.map_canvas.xview_moveto(xf)
            if y - cf * view_ht < yview[0] or y + cf * view_ht > yview[1]:
                yf = y - self.map_canvas.winfo_height()/2.0
                yf = float(yf) / float(ht)
                self.map_canvas.yview_moveto(yf)

    ##################################################################
    # draw & load & save games
    ##################################################################

    def undraw_game(self):
        self.undraw_setup()
        self.undraw_areas()
        self.undraw_map()

    def draw_game(self):
        self.draw_map()
        self.draw_areas()
        self.draw_setup()
        
    def open_load_game_dialog(self, dummyargs=None):
        filename = None
        if (not self.gameconf.ur_stack.can_undo()) or guiutils.askyesno(
                    "Confirm load game", 
                    "Load game cannot be undone. Do you want to continue?"):
            filename = io.open_load_dialog(
                "",
                gameconf.DEFAULT_GAME_FILENAME,
                "Game files",
                title="Load game file")
        if filename:
            self.load_game(filename)

    def load_game(self, filename):
        self.push_cursor("watch")
        m = Meter(self.info_bar, fillcolor="Blue", relief='ridge', bd=3)
        m.pack(fill='x')
        m.wait_visibility() # window needs to be visible for the grab
        m.grab_set()
        m.set(0.15, 'Loading %s' % filename)
        
        self.gameconf.ur_stack.clear()

        newconf, errors = gameconf.load_gameconf_from_archive(filename, root=self)
        m.set(0.75, 'Computing graphical presentation')
        self.pop_cursor()

        if len(errors)>0:
            guiutils.display_error_message("Error(s) while loading zip archive:\n" +
                                  ",\n".join(errors))
        else:
            self.push_cursor("watch")
            self.undraw_game()
            self.redraw_gui_for_gameconf(newconf)
            self.gameconf.game_filename = filename
            self.game_dir = os.path.dirname(filename)
            self.update_window_title(game_title=filename)
            self.pop_cursor()
        m.destroy()

    def load_from_archive(self, archive, fname, func):
        result = True
        try:
            func(StringIO.StringIO(archive.read(fname)))
        except (IOError, zipfile.error, KeyError):
            #guiutils.display_error_message("Could not load %s from zip archive" % fname)
            result = False
        return result

    def remove_editors(self):
        self.remove_colour_pick_editor()
        self.remove_colouring_editor()
        self.remove_area_editor()
        self.remove_setup_editor()

    def create_editors(self):
        self.create_setup_editor()
        self.create_area_editor()
        self.create_colouring_editor()
        self.create_colour_pick_editor()

    def redraw_gui_for_gameconf(self, conf):
        self.remove_editors()
        self.init_gamedata(conf)
        self.current_colour_var.set(":".join(self.gameconf.colours[0]))
        self.colouring.set(False)
        self.create_editors()
        self.show_editor_func()
        self.bind_editor_keys()
        self.draw_game()
        self.update_window_title(game_title=conf.game_filename)
        self.turn_name_var.set(conf.name)

    def open_save_game_dialog(self, dummyargs=None):
        gamefilename = self.gameconf.game_filename
        if gamefilename is None or len(gamefilename)==0:
            gamefilename = gameconf.DEFAULT_GAME_FILENAME
        filename = io.open_save_dialog(
            self.gameconf.game_filename,
            gameconf.DEFAULT_GAME_FILENAME,
            "Game files",
            title="Save game file")
        if filename:
            if self.save_gamezip(filename):
                self.game_dir = os.path.dirname(filename)
                self.update_window_title(game_title=filename)
                self.gameconf.ur_stack.set_unsaved_changes(False)
    
    def save_gamezip(self, game_filename):
        errors = self.gameconf.save(game_filename, 
                                    undostack=self.gameconf.ur_stack.undo_stack,
                                    redostack=self.gameconf.ur_stack.redo_stack)
        if len(errors)>0:
            guiutils.display_error_message("\n".join(errors))
            return False
        return True

    def open_save_setup_as_text_dialog(self):
        filename = io.open_save_dialog(
            "",
            "setup.txt",
            "Setup text files",
            title="Save setup as plain text")
        if filename:
            self.save_setup_as_text(filename)

    def save_setup_as_text(self, filename):
        area_names = self.gameconf.get_area_info().get_area_names()
        area_names.sort()
        result = []
        self.compute_units_in_area()
        for an in area_names:
            result.append(an)
            for u in self.get_units_in_area(an):
                result.append("    %s" % u.name)
                result.append(
                    io.attrs_to_str(
                        u.get_all_attrs(),
                        spc="        "))
        if len(result)>0:
            result.append("")
        try:
            f = file(filename, 'w')
            f.write("\n".join(result))
            f.close()
        except IOError:
            guiutils.display_error_message("Could not save to text file %s" % filename)
            
    def open_save_setup_as_csv_dialog(self):
        filename = io.open_save_dialog(
            "",
            "setup.csv",
            "Setup CSV files",
            title="Save setup as CSV file")
        if filename:
            self.save_setup_as_csv(filename)

    def save_setup_as_csv(self, filename):
        area_names = self.gameconf.get_area_info().get_area_names()
        excluded_unit_attrs = {"name": None}
        area_names.sort()
        uas = set()
        for u in self.gameconf.get_units():
            for k in u.get_all_attrs(excluded_unit_attrs).keys():
                uas.add(k)
        header = [k for k in uas ]
        header.sort()
        self.compute_units_in_area()
        try:
            f = csv.writer(open(filename,'wb'))
            f.writerow(["Area name", "Unit name"] + header)

            for an in area_names:
                for u in self.get_units_in_area(an):
                    row = [an, u.name]
                    uas = u.get_all_attrs(excluded_unit_attrs)
                    row.extend([uas.get(h, "") for h in header ])
                    f.writerow(row)

        except IOError:
            guiutils.display_error_message("Could not save to text file %s" % filename)
        
    def open_load_setup_as_csv_dialog(self):
        filename = io.open_load_dialog(
            "",
            "setup.csv",
            "Setup CSV files",
            title="Load setup from CSV file")
        if filename:
            self.load_setup_from_csv(filename)

    def check_and_make_unit(self, uattrs):
        if (('bitmap' not in uattrs) or (uattrs['bitmap'] == '')):
            uattrs['bitmap'] = self.gameconf.get_default_unit_bitmap()
        uname = uattrs['name']
        game_unit = self.gameconf.get_unit(uname)
        if game_unit is None:
            unit = None
            if self.gameconf.has_required_unit_attrs(uattrs):
                unit = self.gameconf.create_unit_from_dict(uattrs)
            if unit is not None:
                self.add_unit(unit)
            else:
                guiutils.display_error_message(
                    "Unit %s missed (some of) the required properties: %s" % 
                    (uname, str(self.required_unit_attrs())))
        else:
            game_unit.update_attrs(uattrs)
            self.draw_unit(game_unit)

    def load_setup_from_csv(self, filename):
        colname = {}
        try:
            f = csv.reader(open(filename,'r'))
            header = f.next()
            i = 0
            for h in header[2:]:
                colname[i] = h
                i += 1
            for row in f:
                uattrs = {}
                uattrs['name'] = row[1]
                i = 0
                for v in row[2:]:
                    stripped = v.strip()
                    if stripped != "":
                        uattrs[colname[i]] = stripped
                    i += 1
                self.check_and_make_unit(uattrs)
        except IOError:
            guiutils.display_error_message("Could not load from CSV file %s" % filename)

######################################################################
# main
######################################################################
if __name__=="__main__":
    try:

        tkroot = Tkinter.Tk()
        root = Root(tkroot)
        root.mainloop()
        
    except SystemExit, v:
        if v.code != 0:
            traceback.print_exc(v)
    except:
        guiutils.display_error_message("MD could not be started")
        sys.stderr.write("ERROR:\n")
        traceback.print_exc(file=sys.stderr)
        sys.exit(1)
